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

In-App Component #8

Merged
merged 25 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions Knock.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,8 @@ Pod::Spec.new do |spec|
spec.ios.deployment_target = '16.0'
spec.swift_version = '5.3'
spec.source_files = "Sources/**/*"
spec.resource_bundles = {
'Colors' => ['Sources/Resources/Colors.xcassets']
'Media' => ['Sources/Resources/Media.xcassets']
}
end
6 changes: 5 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ let package = Package(
.target(
name: "Knock",
dependencies: ["SwiftPhoenixClient"],
path: "Sources"),
path: "Sources",
resources: [
.process("Resources/Colors.xcassets"),
.process("Resources/Media.xcassets")
]),

.testTarget(
name: "KnockTests",
Expand Down
148 changes: 148 additions & 0 deletions Sources/Components/Feed/FeedNotificationRow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//
// KnockFeedNotificationRow.swift
//
//
// Created by Matt Gardner on 4/12/24.
//

import SwiftUI
import WebKit
import UIKit


extension Knock {
public struct FeedNotificationRow: View {
public var item: Knock.FeedItem
public var theme: FeedNotificationRowTheme = .init()
public var buttonTapAction: (String) -> Void

@State private var dynamicHeight: CGFloat = .zero

private var isRead: Bool {
return item.read_at != nil
}

public init(
item: Knock.FeedItem,
theme: FeedNotificationRowTheme = .init(),
buttonTapAction: @escaping (String) -> Void
) {
self.item = item
self.theme = theme
self.buttonTapAction = buttonTapAction
}

public var body: some View {
VStack(spacing: .zero) {
HStack(alignment: .top, spacing: 12) {

HStack(alignment: .top, spacing: 0) {
Circle()
.frame(width: 8, height: 8)
.foregroundStyle(isRead ? .clear : theme.unreadNotificationCircleColor)

if theme.showAvatarView {
AvatarView(
imageURLString: item.actors?.first?.avatar,
name: item.actors?.first?.name,
backgroundColor: theme.avatarViewTheme.avatarViewBackgroundColor,
font: theme.avatarViewTheme.avatarViewInitialsFont,
textColor: theme.avatarViewTheme.avatarViewInitialsColor,
size: theme.avatarViewTheme.avatarViewSize
)
}
}

VStack(alignment: .leading, spacing: .zero) {
ForEach(Array(item.blocks.enumerated()), id: \.offset) { _, block in
Group {
switch block {
case let block as Knock.MarkdownContentBlock:
markdownContent(block: block)

case let block as Knock.ButtonSetContentBlock:
actionButtonsContent(block: block)
.padding(.bottom, 12)
default:
EmptyView()
}
}
}

if let date = item.inserted_at {
Text(theme.sentAtDateFormatter.string(from: date))
.font(theme.sentAtDateFont)
.foregroundStyle(theme.sentAtDateTextColor)
}
}
}
.padding(.vertical, 12)
.padding(.leading, 8)
.padding(.trailing, 16)

Divider()
.background(KnockColor.Gray.gray4)
}
}

@ViewBuilder
private func markdownContent(block: Knock.MarkdownContentBlock) -> some View {
HtmlView(html: block.rendered)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}

@ViewBuilder
private func actionButtonsContent(block: Knock.ButtonSetContentBlock) -> some View {
HStack {
ForEach(Array(block.buttons.enumerated()), id: \.offset) { _, button in
ActionButton(
title: button.label,
config: button.name == "primary" ? theme.primaryActionButtonConfig : theme.secondaryActionButtonConfig
) {}
.onTapGesture {
buttonTapAction(button.action)
}
}
}
}
}
}


struct FeedNotificationRow_Previews: PreviewProvider {
static var previews: some View {
let markdown = Knock.MarkdownContentBlock(name: "markdown", content: "", rendered: "<p>Hey <strong>Dennis</strong> 👋 - Ian Malcolm completed an activity.</p>")

let markdown2 = Knock.MarkdownContentBlock(name: "markdown", content: "", rendered: "<p>Here's a new notification from <strong>Eleanor Price</strong>:</p><blockquote><p>test message test message test message test mtest message test message test message test message test messageessage test message test message test message test message test message test message test message </p></blockquote>")

let buttons = Knock.ButtonSetContentBlock(name: "buttons", buttons: [Knock.BlockActionButton(label: "Primary", name: "primary", action: ""), Knock.BlockActionButton(label: "Secondary", name: "secondary", action: "")])

let item1 = Knock.FeedItem(__cursor: "", actors: [Knock.User(id: "1", name: "John Doe", email: nil, avatar: nil, phone_number: nil, properties: [:])], activities: [], blocks: [markdown], data: [:], id: "", inserted_at: Date(), interacted_at: nil, clicked_at: nil, link_clicked_at: nil, archived_at: nil, total_activities: 0, total_actors: 0, updated_at: nil)

let item2 = Knock.FeedItem(__cursor: "", actors: [], activities: [], blocks: [markdown, buttons], data: [:], id: "", inserted_at: Date(), interacted_at: nil, clicked_at: nil, link_clicked_at: nil, archived_at: nil, total_activities: 0, total_actors: 0, updated_at: nil)

let item3 = Knock.FeedItem(__cursor: "", actors: [Knock.User(id: "1", name: "John Doe", email: nil, avatar: nil, phone_number: nil, properties: [:])], activities: [], blocks: [markdown2], data: [:], id: "", inserted_at: Date(), interacted_at: nil, clicked_at: nil, link_clicked_at: nil, archived_at: nil, total_activities: 0, total_actors: 0, updated_at: nil)

let item4 = Knock.FeedItem(__cursor: "", actors: [], activities: [], blocks: [markdown2, buttons], data: [:], id: "", inserted_at: Date(), interacted_at: nil, clicked_at: nil, link_clicked_at: nil, archived_at: nil, total_activities: 0, total_actors: 0, updated_at: nil)

List {
Knock.FeedNotificationRow(item: item1) { _ in }
.listRowInsets(EdgeInsets())
.listRowSeparator(.hidden)

Knock.FeedNotificationRow(item: item2) { _ in }
.listRowInsets(EdgeInsets())
.listRowSeparator(.hidden)

Knock.FeedNotificationRow(item: item3) { _ in }
.listRowInsets(EdgeInsets())
.listRowSeparator(.hidden)

Knock.FeedNotificationRow(item: item4) { _ in }
.listRowInsets(EdgeInsets())
.listRowSeparator(.hidden)
}
.listStyle(PlainListStyle())

}
}
65 changes: 65 additions & 0 deletions Sources/Components/Feed/InAppFeedNotificationIconButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// InAppFeedViewModel.swift
//
//
// Created by Matt Gardner on 4/12/24.
//

import SwiftUI

extension Knock {
public struct InAppFeedNotificationIconButton: View {
@EnvironmentObject public var viewModel: InAppFeedViewModel
public var theme = InAppFeedNotificationIconButtonTheme() // Defines the appearance of the feed view and its components.
public var action: () -> Void // A callback to alert you when user taps on the button.

private var countText: String {
let count = theme.notificationCountType == .unread ? viewModel.feed.meta.unreadCount : viewModel.feed.meta.unseenCount
guard theme.showBadgeWithCount, count > 0 else { return "" }
return count > 99 ? "99" : "\(count)"
}

private var showUnreadBadge: Bool {
theme.notificationCountType == .unread ? viewModel.feed.meta.unreadCount > 0 : viewModel.feed.meta.unseenCount > 0
}

private var badgePadding: CGFloat {
return countText.count > 1 ? 4 : 6
}

public init(theme: InAppFeedNotificationIconButtonTheme = InAppFeedNotificationIconButtonTheme(), action: @escaping () -> Void) {
self.theme = theme
self.action = action
}

public var body: some View {
Button(action: action) {
ZStack {
Image(systemName: "bell")
.resizable()
.frame(width: theme.buttonImageSize.width, height: theme.buttonImageSize.height)
.foregroundColor(theme.buttonImageForeground)

if showUnreadBadge {
Text(countText)
.font(theme.badgeCountFont)
.padding(badgePadding)
.foregroundColor(theme.badgeCountColor)
.background(theme.badgeColor)
.clipShape(Circle())
.offset(x: 10, y: -10)
}
}
}
}
}
}

struct InAppFeedNotificationIconButton_Previews: PreviewProvider {
static var previews: some View {
let viewModel = Knock.InAppFeedViewModel()
viewModel.feed.meta.unreadCount = 9

return Knock.InAppFeedNotificationIconButton(action: {}).environmentObject(viewModel)
}
}
Loading