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

Implement tab bar remote message #3665

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3283e6e
Implement tab bar remote message
jotaemepereira Nov 27, 2024
2a880c0
Change production URL
jotaemepereira Dec 12, 2024
62c8426
Add JSON to be used as debug
jotaemepereira Dec 12, 2024
aa916e3
Use raw JSON
jotaemepereira Dec 12, 2024
3f07a6d
Make button as tall as the tab bar
jotaemepereira Dec 12, 2024
2cf6e71
Minor feedback
jotaemepereira Dec 13, 2024
2b74542
More improvements
jotaemepereira Dec 15, 2024
ecc4860
Update JSON link
jotaemepereira Dec 15, 2024
3dadbce
Fix JSON
jotaemepereira Dec 15, 2024
0499ff2
Add unit tests
jotaemepereira Dec 17, 2024
472e37e
Revert JSON changes
jotaemepereira Dec 17, 2024
52a86be
Remove URL extension
jotaemepereira Dec 17, 2024
5aca880
Fix deallocation issue
jotaemepereira Dec 17, 2024
ba6eadf
Fix filtering on remote message NTP
jotaemepereira Dec 17, 2024
5e8c3b9
Move models to RemoteMessaging folder
jotaemepereira Dec 17, 2024
3fb8ec2
Create TabBarRemoteMessagePresentable
jotaemepereira Dec 17, 2024
4914438
Push JSON
jotaemepereira Dec 17, 2024
f8252d1
Add raw JSON for review build
jotaemepereira Dec 17, 2024
f3151be
Address button feedback
jotaemepereira Dec 18, 2024
cde71c3
Make popover resize automatically
jotaemepereira Dec 19, 2024
d68d8a4
Implement new button designs
jotaemepereira Dec 19, 2024
8b91013
Use weak self on closures
jotaemepereira Dec 19, 2024
d56ee4d
Create two publishers: one for NTP and another for tab bar
jotaemepereira Dec 19, 2024
7d54df2
Address minor design feedback around margins
jotaemepereira Dec 19, 2024
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
36 changes: 36 additions & 0 deletions DuckDuckGo.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Response-DDG-Question-96x96.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion DuckDuckGo/HomePage/View/HomePageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,10 @@ extension HomePage.Views {

@ViewBuilder
func remoteMessage() -> some View {
if let remoteMessage = activeRemoteMessageModel.remoteMessage, let modelType = remoteMessage.content, modelType.isSupported {
if let remoteMessage = activeRemoteMessageModel.newTabPageRemoteMessage,
!remoteMessage.isForTabBar,
let modelType = remoteMessage.content,
modelType.isSupported {
ZStack {
RemoteMessageView(viewModel: .init(
messageId: remoteMessage.id,
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/MainWindow/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ final class MainViewController: NSViewController {
self.isBurner = tabCollectionViewModel.isBurner
self.featureFlagger = featureFlagger

tabBarViewController = TabBarViewController.create(tabCollectionViewModel: tabCollectionViewModel)
tabBarViewController = TabBarViewController.create(tabCollectionViewModel: tabCollectionViewModel, activeRemoteMessageModel: NSApp.delegateTyped.activeRemoteMessageModel)
bookmarksBarVisibilityManager = BookmarksBarVisibilityManager(selectedTabPublisher: tabCollectionViewModel.$selectedTabViewModel.eraseToAnyPublisher())

let networkProtectionPopoverManager: NetPPopoverManager = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import RemoteMessaging

extension ActiveRemoteMessageModel: NewTabPageActiveRemoteMessageProviding {
var remoteMessagePublisher: AnyPublisher<RemoteMessageModel?, Never> {
$remoteMessage.dropFirst().eraseToAnyPublisher()
$newTabPageRemoteMessage
.dropFirst()
.eraseToAnyPublisher()
}

func isMessageSupported(_ message: RemoteMessageModel) -> Bool {
Expand Down
26 changes: 25 additions & 1 deletion DuckDuckGo/RemoteMessaging/ActiveRemoteMessageModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ import os.log
*/
final class ActiveRemoteMessageModel: ObservableObject {

@Published var remoteMessage: RemoteMessageModel?
@Published private var remoteMessage: RemoteMessageModel?
@Published var newTabPageRemoteMessage: RemoteMessageModel?
@Published var tabBarRemoteMessage: RemoteMessageModel?
@Published var isViewOnScreen: Bool = false

/**
Expand Down Expand Up @@ -94,6 +96,21 @@ final class ActiveRemoteMessageModel: ObservableObject {
}
.store(in: &cancellables)

$remoteMessage
.sink { [weak self] newMessage in
if let newMessage = newMessage {
if newMessage.isForTabBar {
self?.tabBarRemoteMessage = newMessage
} else {
self?.newTabPageRemoteMessage = newMessage
}
} else {
self?.newTabPageRemoteMessage = nil
self?.tabBarRemoteMessage = nil
}
}
.store(in: &cancellables)

let remoteMessagePublisher = $remoteMessage
.compactMap({ $0 })
.filter { [weak self] _ in self?.isViewOnScreen == true }
Expand Down Expand Up @@ -185,3 +202,10 @@ extension RemoteMessageModelType {
}
}
}

extension RemoteMessageModel {

var isForTabBar: Bool {
return id == TabBarRemoteMessage.tabBarPermanentSurveyRemoteMessageId
}
}
4 changes: 2 additions & 2 deletions DuckDuckGo/RemoteMessaging/RemoteMessagingClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ final class RemoteMessagingClient: RemoteMessagingProcessing {
static let minimumConfigurationRefreshInterval: TimeInterval = 60 * 30
static let endpoint: URL = {
#if DEBUG
URL(string: "https://raw.githubusercontent.com/duckduckgo/remote-messaging-config/main/samples/ios/sample1.json")!
URL(string: "https://www.jsonblob.com/api/1316017217598578688")!
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ This shouldn’t be merged; after approval, I will revert this file to the original URLs.

#else
URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/macos-config.json")!
URL(string: "https://raw.githubusercontent.com/duckduckgo/macos-browser/4914438d37af6e9d4a988b8190cb3ab884f0321a/tab-bar-remote-message.json")!
#endif
}()
}
Expand Down
52 changes: 52 additions & 0 deletions DuckDuckGo/RemoteMessaging/TabBarActiveRemoteMessage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// TabBarActiveRemoteMessage.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Combine
import RemoteMessaging

protocol TabBarRemoteMessageProviding {
var remoteMessagePublisher: AnyPublisher<RemoteMessageModel?, Never> { get }

func markRemoteMessageAsShown() async
func onSurveyOpened() async
func onMessageDismissed() async
}

final class TabBarActiveRemoteMessage: TabBarRemoteMessageProviding {
private let activeRemoteMessageModel: ActiveRemoteMessageModel

var remoteMessagePublisher: AnyPublisher<RemoteMessageModel?, Never> {
activeRemoteMessageModel.$tabBarRemoteMessage.eraseToAnyPublisher()
}

init(activeRemoteMessageModel: ActiveRemoteMessageModel) {
self.activeRemoteMessageModel = activeRemoteMessageModel
}

func markRemoteMessageAsShown() async {
await activeRemoteMessageModel.markRemoteMessageAsShown()
}

func onSurveyOpened() async {
await activeRemoteMessageModel.dismissRemoteMessage(with: .primaryAction)
}

func onMessageDismissed() async {
await activeRemoteMessageModel.dismissRemoteMessage(with: .close)
}
}
118 changes: 118 additions & 0 deletions DuckDuckGo/RemoteMessaging/TabBarRemoteMessageView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
//
// TabBarRemoteMessageView.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import SwiftUI

struct TabBarRemoteMessageView: View {
@State private var wasViewHovered: Bool = false
@State private var wasCloseButtonHovered: Bool = false

let model: TabBarRemoteMessage

let onClose: () -> Void
let onTap: (URL) -> Void
let onHover: () -> Void
let onHoverEnd: () -> Void
let onAppear: () -> Void

var body: some View {
HStack(spacing: 0) {
HStack {
Text(model.buttonTitle)
.font(.system(size: 13))
.fixedSize(horizontal: true, vertical: false)
.foregroundColor(.white)
}
.padding([.leading, .top, .bottom], 8)
.padding(.trailing, 6)
.cornerRadius(8)
.background(wasViewHovered
? Color("PrimaryButtonHover")
: Color("PrimaryButtonRest"))
.onTapGesture { onTap(model.surveyURL) }
.onHover { hovering in
wasViewHovered = hovering

if hovering {
onHover()
} else {
onHoverEnd()
}
}

Divider()
.background(Color.white.opacity(0.3))
.frame(width: 1)
.padding([.top, .bottom], 3)

HStack {
Image(.close)
.resizable()
.scaledToFit()
.foregroundColor(.white)
.frame(width: 16, height: 16)
}
.padding([.top, .bottom])
.padding([.leading, .trailing], 4)
.background(wasCloseButtonHovered
? Color("PrimaryButtonHover")
: Color("PrimaryButtonRest"))
.cornerRadius(8)
.onTapGesture {
onClose()
}
.onHover { hovering in
wasCloseButtonHovered = hovering
}
.frame(maxWidth: .infinity)
}
.background(wasCloseButtonHovered || wasViewHovered
? Color("PrimaryButtonHover")
: Color("PrimaryButtonRest"))
.frame(height: 24)
.cornerRadius(8)
.onAppear(perform: { onAppear() })
}
}

struct TabBarRemoteMessagePopoverContent: View {
let model: TabBarRemoteMessage

var body: some View {
HStack(alignment: .center, spacing: 12) {
Image(.daxResponse)
.resizable()
.scaledToFit()
.frame(width: 72, height: 72)

VStack(alignment: .leading, spacing: 8) {
Text(model.popupTitle)
.font(.system(size: 13, weight: .bold))
.padding(.top, 9)

Text(model.popupSubtitle)
.font(.system(size: 13, weight: .medium))
.padding(.bottom, 9)
}
}
.frame(width: 360)
Copy link
Collaborator Author

@jotaemepereira jotaemepereira Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Now, we only fix the width of the popover; if the text is larger, the popover should increase its height.

.padding([.top, .bottom], 10)
.padding(.leading, 12)
.padding(.trailing, 24)
}
}
26 changes: 26 additions & 0 deletions DuckDuckGo/TabBar/Model/TabBarRemoteMessage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// TabBarRemoteMessage.swift
//
// Copyright © 2024 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

struct TabBarRemoteMessage {
static let tabBarPermanentSurveyRemoteMessageId = "macos_permanent_survey_tab_bar"

let buttonTitle: String
let popupTitle: String
let popupSubtitle: String
let surveyURL: URL
}
Loading
Loading