Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Notifications Inbox Screen Implementation #113

Merged
merged 9 commits into from
Jan 21, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Core/Core.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@
14DE13DA2BDF7E5E0059168F /* IAPConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DE13D92BDF7E5E0059168F /* IAPConfig.swift */; };
14DE13DC2BDF83350059168F /* ServerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14DE13DB2BDF83350059168F /* ServerConfig.swift */; };
14F8E66B2C0F4D1200E4EF69 /* RestoreInProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F8E66A2C0F4D1200E4EF69 /* RestoreInProgressView.swift */; };
973CCD7B2D3660D100949756 /* HTML2TextParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 973CCD792D3660D100949756 /* HTML2TextParser.swift */; };
973CCD7C2D3660D100949756 /* AttributedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 973CCD782D3660D100949756 /* AttributedText.swift */; };
9784D47E2BF7762800AFEFFF /* FullScreenErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */; };
A51CDBE72B6D21F2009B6D4E /* SegmentConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51CDBE62B6D21F2009B6D4E /* SegmentConfig.swift */; };
A53A32352B233DEC005FE38A /* ThemeConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A32342B233DEC005FE38A /* ThemeConfig.swift */; };
Expand Down Expand Up @@ -412,6 +414,8 @@
349B90CD6579F7B8D257E515 /* Pods_App_Core.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App_Core.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B74C6685E416657F3C5F5A8 /* Pods-App-Core.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.releaseprod.xcconfig"; sourceTree = "<group>"; };
60153262DBC2F9E660D7E11B /* Pods-App-Core.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.release.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.release.xcconfig"; sourceTree = "<group>"; };
973CCD782D3660D100949756 /* AttributedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedText.swift; sourceTree = "<group>"; };
973CCD792D3660D100949756 /* HTML2TextParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTML2TextParser.swift; sourceTree = "<group>"; };
9784D47D2BF7762800AFEFFF /* FullScreenErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullScreenErrorView.swift; sourceTree = "<group>"; };
9D5B06CAA99EA5CD49CBE2BB /* Pods-App-Core.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Core.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Core/Pods-App-Core.debugdev.xcconfig"; sourceTree = "<group>"; };
A51CDBE62B6D21F2009B6D4E /* SegmentConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentConfig.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -846,6 +850,7 @@
0770DE7728D0C49E006D8A5D /* Base */ = {
isa = PBXGroup;
children = (
973CCD7A2D3660D100949756 /* AttributedText */,
9784D47C2BF7761F00AFEFFF /* FullScreenErrorView */,
064987882B4D69FE0071642A /* Webview */,
A5CAA1EB2C526901007690D9 /* SecureInputView.swift */,
Expand Down Expand Up @@ -948,6 +953,15 @@
path = ServerConfig;
sourceTree = "<group>";
};
973CCD7A2D3660D100949756 /* AttributedText */ = {
isa = PBXGroup;
children = (
973CCD782D3660D100949756 /* AttributedText.swift */,
973CCD792D3660D100949756 /* HTML2TextParser.swift */,
);
path = AttributedText;
sourceTree = "<group>";
};
9784D47C2BF7761F00AFEFFF /* FullScreenErrorView */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1396,6 +1410,8 @@
0236961D28F9A2D200EEF206 /* Data_AuthResponse.swift in Sources */,
A5F4E7B52B61544A00ACD166 /* BrazeConfig.swift in Sources */,
06DEA4A52BBD66D700110D20 /* BackNavigationButtonViewModel.swift in Sources */,
973CCD7B2D3660D100949756 /* HTML2TextParser.swift in Sources */,
973CCD7C2D3660D100949756 /* AttributedText.swift in Sources */,
02AFCC182AEFDB24000360F0 /* ThirdPartyMailClient.swift in Sources */,
0233D5712AF13EC800BAC8BD /* SelectMailClientView.swift in Sources */,
14A8325B2BD7615C00E5654D /* CourseUpgradeRepository.swift in Sources */,
Expand Down
73 changes: 73 additions & 0 deletions Core/Core/View/Base/AttributedText/AttributedText.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import SwiftUI

/**
AttributedText is a view for displaying some HTML-tagged text using SwiftUI Text View.

- warning: **Only single-word tags are supported**. Tags with more than one word or
containing any characters besides **letters** or **numbers** are ignored and not removed.

# Notes
1. Basic modifiers can still be applied, such as changing the font and color of the text.
2. Handles unopened/unclosed tags.
3. Supports overlapping tags.
4. Deletes tags that have no modifiers.
5. Does **not** handle HTML characters such as `&amp;`.

# Example
```
AttributedText("This is <b>bold</b> and <i>italic</i> text.")
.foregroundColor(.blue)
.font(.title)
.padding()
```
*/
public struct AttributedText: View {
/// Set of supported tags and associated modifiers. This is used by default for all AttributedText
/// instances except those for which this parameter is defined in the initializer.
public static var tags: [String: (Text) -> (Text)] = [
// This modifier set is presented just for reference.
// Set the necessary attributes and modifiers for your needs before use.
"h1": { $0.font(.largeTitle) },
"h2": { $0.font(.title) },
"h3": { $0.font(.headline) },
"h4": { $0.font(.subheadline) },
"h5": { $0.font(.callout) },
"h6": { $0.font(.caption) },

"i": { $0.italic() },
"u": { $0.underline() },
"s": { $0.strikethrough() },
"b": { $0.fontWeight(.bold) },
"strong": { $0.fontWeight(.semibold) },

"sup": { $0.baselineOffset(10).font(.footnote) },
"sub": { $0.baselineOffset(-10).font(.footnote) }
]
/// Parser formatted text.
private let text: Text

/**
Creates a text view that displays formatted content.

- parameter htmlString: HTML-tagged string.
- parameter tags: Set of supported tags and associated modifiers for a particular instance.
*/
public init(_ htmlString: String, tags: [String: (Text) -> (Text)]? = nil) {
let parser = HTML2TextParser(htmlString, availableTags: tags == nil ? AttributedText.tags : tags!)
parser.parse()
text = parser.formattedText
}

public var body: some View {
text
}
}

struct AttributedText_Previews: PreviewProvider {
static var previews: some View {
AttributedText("This is <b>bold</b> and <i>italic</i> text.")
.foregroundColor(.blue)
.font(.title)
.padding()
}
}
103 changes: 103 additions & 0 deletions Core/Core/View/Base/AttributedText/HTML2TextParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import SwiftUI

/**
Parser for converting HTML-tagged text to SwiftUI Text View.

- warning: **Only single-word tags are supported**. Tags with more than one word or
containing any characters besides **letters** or **numbers** are ignored and not removed.

# Notes:
1. Handles unopened/unclosed tags.
2. Deletes tags that have no modifiers.
3. Does **not** handle HTML characters, for example `&lt;`.
*/
internal class HTML2TextParser {
/// The result of the parser's work.
internal private(set) var formattedText = Text("")
/// HTML-tagged text.
private let htmlString: String
/// Set of currently active tags.
private var tags: Set<String> = []
/// Set of supported tags and associated modifiers.
private let availableTags: [String: (Text) -> (Text)]

/**
Creates a new parser instance.

- parameter htmlString: HTML-tagged string.
- parameter availableTags: Set of supported tags and associated modifiers.
*/
internal init(_ htmlString: String, availableTags: [String: (Text) -> (Text)]) {
self.htmlString = htmlString
self.availableTags = availableTags
}

/// Starts the text parsing process. The results of this method will be placed in the `formattedText` variable.
internal func parse() {
var tag: String?
var endTag: Bool = false
var startIndex = htmlString.startIndex
var endIndex = htmlString.startIndex

for index in htmlString.indices {
switch htmlString[index] {
case "<":
tag = String()
endIndex = index
continue

case "/":
if index != htmlString.startIndex && htmlString[htmlString.index(before: index)] == "<" {
endTag = true
} else {
tag = nil
}
continue

case ">":
if let tag = tag {
addChunkOfText(String(htmlString[startIndex..<endIndex]))
if endTag {
tags.remove(tag.lowercased())
endTag = false
} else {
tags.insert(tag.lowercased())
}
startIndex = htmlString.index(after: index)
}
tag = nil
continue

default:
break
}

if tag != nil {
if htmlString[index].isLetter || htmlString[index].isHexDigit {
tag?.append(htmlString[index])
} else {
tag = nil
}
}
}

endIndex = htmlString.endIndex
if startIndex != endIndex {
addChunkOfText(String(htmlString[startIndex..<endIndex]))
}
}

private func addChunkOfText(_ string: String) {
guard !string.isEmpty else { return }
var textChunk = Text(string)

for tag in tags {
if let action = availableTags[tag] {
textChunk = action(textChunk)
}
}

let text = formattedText + textChunk
formattedText = text
}
}
20 changes: 20 additions & 0 deletions Dashboard/Dashboard.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
02F6EF4328D9ECC500835477 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 02F6EF4528D9ECC500835477 /* Localizable.strings */; };
02F6EF4828D9ED8300835477 /* Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02F6EF4728D9ED8300835477 /* Strings.swift */; };
214DA1AADABC7BF4FB8EA1D7 /* Pods_App_Dashboard.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0B008B2F0762EF35CADE3DD4 /* Pods_App_Dashboard.framework */; };
97D2CBB92D354DCA006AC2C2 /* Notifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 97D2CBB82D354DCA006AC2C2 /* Notifications.framework */; };
mta452 marked this conversation as resolved.
Show resolved Hide resolved
97D2CBBA2D354DCA006AC2C2 /* Notifications.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 97D2CBB82D354DCA006AC2C2 /* Notifications.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
97E7DF0B2B7A3EAF00A2A09B /* CourseEnrollmentsMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97E7DF0A2B7A3EAF00A2A09B /* CourseEnrollmentsMock.swift */; };
9AD4A6A1AAF97092CF457FE2 /* Pods_App_Dashboard_DashboardTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 22905947A936093AD23D4CF8 /* Pods_App_Dashboard_DashboardTests.framework */; };
/* End PBXBuildFile section */
Expand All @@ -48,6 +50,20 @@
};
/* End PBXContainerItemProxy section */

/* Begin PBXCopyFilesBuildPhase section */
97D2CBBB2D354DCA006AC2C2 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
97D2CBBA2D354DCA006AC2C2 /* Notifications.framework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
0277241D2BCE9E1500C2908D /* PrimaryCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrimaryCardView.swift; sourceTree = "<group>"; };
0277241F2BCEA16C00C2908D /* ProgressLineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressLineView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -89,6 +105,7 @@
65CD5AB152F3DEC88D48AE2D /* Pods-App-Dashboard.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Dashboard.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Dashboard/Pods-App-Dashboard.debugdev.xcconfig"; sourceTree = "<group>"; };
6BA3D1943CAB859ECDC95C76 /* Pods-App-Dashboard.releasedev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Dashboard.releasedev.xcconfig"; path = "Target Support Files/Pods-App-Dashboard/Pods-App-Dashboard.releasedev.xcconfig"; sourceTree = "<group>"; };
89D6F05AC9854DBBAB091D9C /* Pods-App-Dashboard.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Dashboard.release.xcconfig"; path = "Target Support Files/Pods-App-Dashboard/Pods-App-Dashboard.release.xcconfig"; sourceTree = "<group>"; };
97D2CBB82D354DCA006AC2C2 /* Notifications.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Notifications.framework; sourceTree = BUILT_PRODUCTS_DIR; };
97E7DF0A2B7A3EAF00A2A09B /* CourseEnrollmentsMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CourseEnrollmentsMock.swift; sourceTree = "<group>"; };
BBABB135366FFB1DAEFA0D16 /* Pods-App-Dashboard-DashboardTests.debugprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Dashboard-DashboardTests.debugprod.xcconfig"; path = "Target Support Files/Pods-App-Dashboard-DashboardTests/Pods-App-Dashboard-DashboardTests.debugprod.xcconfig"; sourceTree = "<group>"; };
CCF4C665AD91B6B96F6A11DF /* Pods-App-Dashboard-DashboardTests.debugdev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Dashboard-DashboardTests.debugdev.xcconfig"; path = "Target Support Files/Pods-App-Dashboard-DashboardTests/Pods-App-Dashboard-DashboardTests.debugdev.xcconfig"; sourceTree = "<group>"; };
Expand All @@ -112,6 +129,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
97D2CBB92D354DCA006AC2C2 /* Notifications.framework in Frameworks */,
027DB33F28D8E605002B6862 /* Core.framework in Frameworks */,
214DA1AADABC7BF4FB8EA1D7 /* Pods_App_Dashboard.framework in Frameworks */,
);
Expand Down Expand Up @@ -246,6 +264,7 @@
1E3E639E65A1C88EDE136CC9 /* Frameworks */ = {
isa = PBXGroup;
children = (
97D2CBB82D354DCA006AC2C2 /* Notifications.framework */,
027DB33E28D8E605002B6862 /* Core.framework */,
0B008B2F0762EF35CADE3DD4 /* Pods_App_Dashboard.framework */,
22905947A936093AD23D4CF8 /* Pods_App_Dashboard_DashboardTests.framework */,
Expand Down Expand Up @@ -328,6 +347,7 @@
02EF39E328D89F560058F6BD /* Sources */,
02EF39E428D89F560058F6BD /* Frameworks */,
02EF39E528D89F560058F6BD /* Resources */,
97D2CBBB2D354DCA006AC2C2 /* Embed Frameworks */,
);
buildRules = (
);
Expand Down
Loading
Loading