Skip to content

Commit db6bd8e

Browse files
authored
Merge pull request #6160 from woocommerce/feat/5954-inbox-row
Inbox view part 1: inbox note row UI - title, HTML content, actions
2 parents 74edde5 + 1757480 commit db6bd8e

File tree

10 files changed

+333
-6
lines changed

10 files changed

+333
-6
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Foundation
2+
3+
extension NSAttributedString {
4+
/// Returns an `NSAttributedString` with attributes applied to the whole string.
5+
func addingAttributes(_ attributes: [NSAttributedString.Key: Any]) -> NSAttributedString {
6+
let attributedHTMLString = NSMutableAttributedString(attributedString: self)
7+
8+
let range = NSRange(location: 0, length: attributedHTMLString.length)
9+
attributedHTMLString.addAttributes(attributes, range: range)
10+
return attributedHTMLString
11+
}
12+
}

WooCommerce/Classes/ViewRelated/Inbox/Inbox.swift

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@ struct Inbox: View {
1515
Group {
1616
switch viewModel.syncState {
1717
case .results:
18-
// TODO: 5954 - update results state
1918
InfiniteScrollList(isLoading: viewModel.shouldShowBottomActivityIndicator,
2019
loadAction: viewModel.onLoadNextPageAction) {
21-
ForEach(viewModel.notes, id: \.id) { note in
22-
TitleAndSubtitleRow(title: note.title, subtitle: note.content)
20+
ForEach(viewModel.noteRowViewModels) { rowViewModel in
21+
if #available(iOS 15.0, *) {
22+
// In order to show full-width separator, the default list separator is hidden and a `Divider` is shown inside the row.
23+
InboxNoteRow(viewModel: rowViewModel)
24+
.listRowSeparator(.hidden)
25+
} else {
26+
InboxNoteRow(viewModel: rowViewModel)
27+
}
2328
}
2429
}
2530
case .empty:
@@ -29,8 +34,14 @@ struct Inbox: View {
2934
image: .emptyProductsTabImage)
3035
.frame(maxHeight: .infinity)
3136
case .syncingFirstPage:
32-
// TODO: 5954 - update placeholder state
33-
EmptyView()
37+
List {
38+
ForEach(viewModel.placeholderRowViewModels) { rowViewModel in
39+
InboxNoteRow(viewModel: rowViewModel)
40+
.redacted(reason: .placeholder)
41+
.shimmering()
42+
}
43+
}
44+
.listStyle(PlainListStyle())
3445
}
3546
}
3647
.background(Color(.listBackground).ignoresSafeArea())
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import SwiftUI
2+
import Yosemite
3+
4+
/// Shows information about an inbox note with actions and a CTA to dismiss the note.
5+
struct InboxNoteRow: View {
6+
let viewModel: InboxNoteRowViewModel
7+
8+
var body: some View {
9+
VStack(spacing: 0) {
10+
VStack(alignment: .leading,
11+
spacing: Constants.verticalSpacing) {
12+
// HStack with type icon and relative date.
13+
// TODO: 5954 - type icon and relative date
14+
15+
// Title.
16+
Text(viewModel.title)
17+
.bodyStyle()
18+
.fixedSize(horizontal: false, vertical: true)
19+
20+
// Content.
21+
AttributedText(viewModel.attributedContent)
22+
.attributedTextLinkColor(Color(.accent))
23+
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)")
31+
}
32+
.foregroundColor(Color(.accent))
33+
.font(.body)
34+
.buttonStyle(PlainButtonStyle())
35+
} else {
36+
Text(action.title)
37+
}
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())
46+
47+
Spacer()
48+
}
49+
}
50+
.padding(Constants.defaultPadding)
51+
52+
if #available(iOS 15.0, *) {
53+
// In order to show full-width separator, the default list separator is hidden and a `Divider` is shown inside the row.
54+
Divider()
55+
.frame(height: Constants.dividerHeight)
56+
}
57+
}
58+
.listRowInsets(.zero)
59+
}
60+
}
61+
62+
private extension InboxNoteRow {
63+
enum Localization {
64+
static let dismiss = NSLocalizedString("Dismiss", comment: "Dismiss button in inbox note row.")
65+
}
66+
67+
enum Constants {
68+
static let spacingBetweenActions: CGFloat = 16
69+
static let verticalSpacing: CGFloat = 14
70+
static let defaultPadding: CGFloat = 16
71+
static let dividerHeight: CGFloat = 1
72+
}
73+
}
74+
75+
struct InboxNoteRow_Previews: PreviewProvider {
76+
static var previews: some View {
77+
let note = InboxNote(siteID: 2,
78+
id: 6,
79+
name: "",
80+
type: "",
81+
status: "",
82+
actions: [.init(id: 2, name: "", label: "Let your customers know about Apple Pay", status: "", url: "https://wordpress.org"),
83+
.init(id: 6, name: "", label: "No URL", status: "", url: "")],
84+
title: "Boost sales this holiday season with Apple Pay!",
85+
content: """
86+
Increase your conversion rate by letting your customers know that you accept Apple Pay.
87+
It’s seamless to <a href=\"https://docs.woocommerce.com/document/payments/apple-pay/\">
88+
enable Apple Pay with WooCommerce Payments</a> and easy to communicate it with
89+
this <a href=\"https://developer.apple.com/apple-pay/marketing/\">marketing guide</a>.
90+
""",
91+
isRemoved: false,
92+
isRead: false,
93+
dateCreated: .init())
94+
let viewModel = InboxNoteRowViewModel(note: note)
95+
Group {
96+
InboxNoteRow(viewModel: viewModel)
97+
.preferredColorScheme(.dark)
98+
InboxNoteRow(viewModel: viewModel)
99+
.preferredColorScheme(.light)
100+
InboxNoteRow(viewModel: viewModel)
101+
.preferredColorScheme(.light)
102+
.environment(\.sizeCategory, .extraExtraExtraLarge)
103+
}
104+
}
105+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import Yosemite
2+
3+
/// View model for `InboxNoteRow`.
4+
struct InboxNoteRowViewModel: Identifiable, Equatable {
5+
let id: Int64
6+
let title: String
7+
8+
/// HTML note content.
9+
let attributedContent: NSAttributedString
10+
11+
let actions: [InboxNoteRowActionViewModel]
12+
13+
init(note: InboxNote) {
14+
let attributedContent = note.content.htmlToAttributedString
15+
.addingAttributes([
16+
.font: UIFont.body,
17+
.foregroundColor: UIColor.secondaryLabel
18+
])
19+
let actions = note.actions.map { InboxNoteRowActionViewModel(action: $0) }
20+
self.init(id: note.id,
21+
title: note.title,
22+
attributedContent: attributedContent,
23+
actions: actions)
24+
}
25+
26+
init(id: Int64, title: String, attributedContent: NSAttributedString, actions: [InboxNoteRowActionViewModel]) {
27+
self.id = id
28+
self.title = title
29+
self.attributedContent = attributedContent
30+
self.actions = actions
31+
}
32+
}
33+
34+
/// View model for an action in `InboxNoteRow`.
35+
struct InboxNoteRowActionViewModel: Identifiable, Equatable {
36+
let id: Int64
37+
let title: String
38+
let url: URL?
39+
40+
init(action: InboxAction) {
41+
let url = URL(string: action.url)
42+
self.init(id: action.id, title: action.label, url: url)
43+
}
44+
45+
init(id: Int64, title: String, url: URL?) {
46+
self.id = id
47+
self.title = title
48+
self.url = url
49+
}
50+
}

WooCommerce/Classes/ViewRelated/Inbox/InboxViewModel.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,18 @@ final class InboxViewModel: ObservableObject {
88
let onLoadTrigger: PassthroughSubject<Void, Never> = PassthroughSubject()
99

1010
/// All inbox notes.
11-
@Published private(set) var notes: [InboxNote] = []
11+
@Published private var notes: [InboxNote] = []
12+
13+
/// View models for inbox note rows.
14+
@Published private(set) var noteRowViewModels: [InboxNoteRowViewModel] = []
15+
16+
/// View models for placeholder rows.
17+
let placeholderRowViewModels: [InboxNoteRowViewModel] = [Int64](0..<3).map {
18+
// The content does not matter because the text in placeholder rows is redacted.
19+
InboxNoteRowViewModel(id: $0, title: " ",
20+
attributedContent: .init(string: "\n\n\n"),
21+
actions: [.init(id: 0, title: "Placeholder", url: nil)])
22+
}
1223

1324
// MARK: Sync
1425

@@ -36,6 +47,8 @@ final class InboxViewModel: ObservableObject {
3647
self.syncState = syncState
3748
self.paginationTracker = PaginationTracker(pageSize: pageSize)
3849

50+
$notes.map { $0.map { InboxNoteRowViewModel(note: $0) } }.assign(to: &$noteRowViewModels)
51+
3952
configurePaginationTracker()
4053
configureFirstPageLoad()
4154
}

WooCommerce/Classes/ViewRelated/ReusableViews/SwiftUI Components/AttributedText.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ import SwiftUI
2525
SOFTWARE.
2626
*/
2727

28+
/// Note: the font and foreground color of the text have to be set in `NSAttributedString`'s attributes.
29+
/// `font` and `attributedTextForegroundColor` functions do not take effect.
30+
/// The link color can be set with `attributedTextLinkColor`.
2831
struct AttributedText: View {
2932
@StateObject private var textViewStore = TextViewStore()
3033

WooCommerce/WooCommerce.xcodeproj/project.pbxproj

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,11 @@
211211
02645D7D27BA027B0065DC68 /* Inbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D7927BA027B0065DC68 /* Inbox.swift */; };
212212
02645D7E27BA027B0065DC68 /* InboxViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D7A27BA027B0065DC68 /* InboxViewModel.swift */; };
213213
02645D8227BA20A30065DC68 /* InboxViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D8127BA20A30065DC68 /* InboxViewModelTests.swift */; };
214+
02645D8527BA2DB40065DC68 /* InboxNoteRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D8327BA2DB30065DC68 /* InboxNoteRowViewModel.swift */; };
215+
02645D8627BA2DB40065DC68 /* InboxNoteRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D8427BA2DB40065DC68 /* InboxNoteRow.swift */; };
216+
02645D8827BA2E820065DC68 /* NSAttributedString+Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D8727BA2E820065DC68 /* NSAttributedString+Attributes.swift */; };
217+
02645D8A27BA2EDB0065DC68 /* NSAttributedString+AttributesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D8927BA2EDB0065DC68 /* NSAttributedString+AttributesTests.swift */; };
218+
02645D8C27BA342D0065DC68 /* InboxNoteRowViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02645D8B27BA342D0065DC68 /* InboxNoteRowViewModelTests.swift */; };
214219
02691780232600A6002AFC20 /* ProductsTabProductViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0269177F232600A6002AFC20 /* ProductsTabProductViewModelTests.swift */; };
215220
02691782232605B9002AFC20 /* PaginatedListViewControllerStateCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02691781232605B9002AFC20 /* PaginatedListViewControllerStateCoordinatorTests.swift */; };
216221
0269576A23726304001BA0BF /* KeyboardFrameObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0269576923726304001BA0BF /* KeyboardFrameObserver.swift */; };
@@ -1839,6 +1844,11 @@
18391844
02645D7927BA027B0065DC68 /* Inbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Inbox.swift; sourceTree = "<group>"; };
18401845
02645D7A27BA027B0065DC68 /* InboxViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InboxViewModel.swift; sourceTree = "<group>"; };
18411846
02645D8127BA20A30065DC68 /* InboxViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxViewModelTests.swift; sourceTree = "<group>"; };
1847+
02645D8327BA2DB30065DC68 /* InboxNoteRowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InboxNoteRowViewModel.swift; sourceTree = "<group>"; };
1848+
02645D8427BA2DB40065DC68 /* InboxNoteRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InboxNoteRow.swift; sourceTree = "<group>"; };
1849+
02645D8727BA2E820065DC68 /* NSAttributedString+Attributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Attributes.swift"; sourceTree = "<group>"; };
1850+
02645D8927BA2EDB0065DC68 /* NSAttributedString+AttributesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+AttributesTests.swift"; sourceTree = "<group>"; };
1851+
02645D8B27BA342D0065DC68 /* InboxNoteRowViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxNoteRowViewModelTests.swift; sourceTree = "<group>"; };
18421852
0269177F232600A6002AFC20 /* ProductsTabProductViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductsTabProductViewModelTests.swift; sourceTree = "<group>"; };
18431853
02691781232605B9002AFC20 /* PaginatedListViewControllerStateCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginatedListViewControllerStateCoordinatorTests.swift; sourceTree = "<group>"; };
18441854
0269576923726304001BA0BF /* KeyboardFrameObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardFrameObserver.swift; sourceTree = "<group>"; };
@@ -3760,6 +3770,8 @@
37603770
children = (
37613771
02645D7927BA027B0065DC68 /* Inbox.swift */,
37623772
02645D7A27BA027B0065DC68 /* InboxViewModel.swift */,
3773+
02645D8427BA2DB40065DC68 /* InboxNoteRow.swift */,
3774+
02645D8327BA2DB30065DC68 /* InboxNoteRowViewModel.swift */,
37633775
);
37643776
path = Inbox;
37653777
sourceTree = "<group>";
@@ -3768,6 +3780,7 @@
37683780
isa = PBXGroup;
37693781
children = (
37703782
02645D8127BA20A30065DC68 /* InboxViewModelTests.swift */,
3783+
02645D8B27BA342D0065DC68 /* InboxNoteRowViewModelTests.swift */,
37713784
);
37723785
path = Inbox;
37733786
sourceTree = "<group>";
@@ -6040,6 +6053,7 @@
60406053
DE67D46826BAA82600EFE8DB /* Publisher+WithLatestFromTests.swift */,
60416054
DE7842EE26F079A60030C792 /* NumberFormatter+LocalizedTests.swift */,
60426055
02DE5CAA279F8754007CBEF3 /* Double+RoundingTests.swift */,
6056+
02645D8927BA2EDB0065DC68 /* NSAttributedString+AttributesTests.swift */,
60436057
);
60446058
path = Extensions;
60456059
sourceTree = "<group>";
@@ -6655,6 +6669,7 @@
66556669
DE7842F626F2E9340030C792 /* UIViewController+Connectivity.swift */,
66566670
DEC51B05276B3F3C009F3DF4 /* Int64+Helpers.swift */,
66576671
02DE5CA8279F857D007CBEF3 /* Double+Rounding.swift */,
6672+
02645D8727BA2E820065DC68 /* NSAttributedString+Attributes.swift */,
66586673
);
66596674
path = Extensions;
66606675
sourceTree = "<group>";
@@ -8222,6 +8237,7 @@
82228237
57896D6625362B0C000E8C4D /* TitleAndEditableValueTableViewCellViewModel.swift in Sources */,
82238238
26E1BECE251CD9F80096D0A1 /* RefundItemViewModel.swift in Sources */,
82248239
DE7B479027A153C20018742E /* CouponSearchUICommand.swift in Sources */,
8240+
02645D8827BA2E820065DC68 /* NSAttributedString+Attributes.swift in Sources */,
82258241
024DF3092372CA00006658FE /* EditorViewProperties.swift in Sources */,
82268242
45F8C43B2680BC3500F1A6EC /* ShippingLabelDiscountInfoViewController.swift in Sources */,
82278243
DE77889A26FD7EF0008DFF44 /* ShippingLabelPackageItem.swift in Sources */,
@@ -8775,6 +8791,7 @@
87758791
934CB123224EAB150005CCB9 /* main.swift in Sources */,
87768792
456396AE25C81D81001F1A26 /* ShippingLabelFormViewModel.swift in Sources */,
87778793
02ECD1DF24FF48D000735BE5 /* PaginationTracker.swift in Sources */,
8794+
02645D8627BA2DB40065DC68 /* InboxNoteRow.swift in Sources */,
87788795
020886572499E643001D784E /* ProductExternalLinkViewController.swift in Sources */,
87798796
262A09812628A8F40033AD20 /* WooStyleModifiers.swift in Sources */,
87808797
DEC2962526C122DF005A056B /* ShippingLabelCustomsFormInputViewModel.swift in Sources */,
@@ -8921,6 +8938,7 @@
89218938
26FE09DD24D9F3F600B9BDF5 /* LoadingView.swift in Sources */,
89228939
457151AB243B6E8000EB2DFA /* ProductSlugViewController.swift in Sources */,
89238940
02F49ADA23BF356E00FA0BFA /* TitleAndTextFieldTableViewCell.ViewModel+State.swift in Sources */,
8941+
02645D8527BA2DB40065DC68 /* InboxNoteRowViewModel.swift in Sources */,
89248942
740382DB2267D94100A627F4 /* LargeImageTableViewCell.swift in Sources */,
89258943
CE22709F2293052700C0626C /* WebviewHelper.swift in Sources */,
89268944
02BA12852461674B008D8325 /* Optional+String.swift in Sources */,
@@ -9320,6 +9338,7 @@
93209338
26B119C024D0C69500FED5C7 /* SurveyViewControllerTests.swift in Sources */,
93219339
D8AB131E225DC25F002BB5D1 /* MockOrders.swift in Sources */,
93229340
D8025496265530E0001B2CC1 /* CardPresentModalFoundReaderTests.swift in Sources */,
9341+
02645D8C27BA342D0065DC68 /* InboxNoteRowViewModelTests.swift in Sources */,
93239342
D85136D5231E40B500DD0539 /* ProductReviewTableViewCellTests.swift in Sources */,
93249343
45FBDF2B238BF8A300127F77 /* ProductImageCollectionViewCellTests.swift in Sources */,
93259344
D89C009A25B4EEA4000E4683 /* WrongAccountErrorViewModelTests.swift in Sources */,
@@ -9442,6 +9461,7 @@
94429461
D8736B5122EB69E300A14A29 /* OrderDetailsViewModelTests.swift in Sources */,
94439462
02C0CD2E23B5E3AE00F880B1 /* DefaultImageServiceTests.swift in Sources */,
94449463
02E4FD812306AA890049610C /* StatsTimeRangeBarViewModelTests.swift in Sources */,
9464+
02645D8A27BA2EDB0065DC68 /* NSAttributedString+AttributesTests.swift in Sources */,
94459465
6856D2A5C2076F5BF14F2C11 /* KeyboardStateProviderTests.swift in Sources */,
94469466
6856D806DE7DB61522D54044 /* NSMutableAttributedStringHelperTests.swift in Sources */,
94479467
023D69442588C6BD00F7DA72 /* ShippingLabelPaperSizeListSelectorCommandTests.swift in Sources */,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import XCTest
2+
import UIKit
3+
@testable import WooCommerce
4+
5+
final class NSAttributedString_AttributesTests: XCTestCase {
6+
func test_adding_attributes_applies_to_the_whole_range() {
7+
// Given
8+
let originalString = NSAttributedString(string: "It’s seamless to <a href=\"https://docs.woocommerce.com\">enable Apple Pay with Stripe</a>")
9+
10+
// When
11+
let string = originalString.addingAttributes([.foregroundColor: UIColor.purple])
12+
13+
// Then
14+
var effectiveRange = NSRange()
15+
XCTAssertEqual(string.attributes(at: 0, effectiveRange: &effectiveRange) as? [NSAttributedString.Key: UIColor], [
16+
.foregroundColor: UIColor.purple
17+
])
18+
XCTAssertEqual(effectiveRange, .init(location: 0, length: originalString.length))
19+
}
20+
}

0 commit comments

Comments
 (0)