Skip to content

Commit

Permalink
Implement tab bar remote message (#3665)
Browse files Browse the repository at this point in the history
Task/Issue URL:
https://app.asana.com/0/1204006570077678/1208970712110808/f
Tech Design URL: 
CC:

**Description**
Adds a new remote message that is shown in the tab bar.

**Steps to test this PR**:
1. Go to the `RemoteMessagingClient` and change the DEBUG endpoint URL
to `https://www.jsonblob.com/api/1316017217598578688`
2. Run the app
3. You should see the new blue button in the tab bar
4. When hovering it, it should show a popup.
5. Tapping the button will take you to the survey
6. Test that when the button is dismissed is removed in all windows
7. Test that New Tab Page does not show this new message

**Definition of Done**:

* [x] Does this PR satisfy our [Definition of
Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)?

—
###### Internal references:
[Pull Request Review
Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f)
[Software Engineering
Expectations](https://app.asana.com/0/59792373528535/199064865822552)
[Technical Design
Template](https://app.asana.com/0/59792373528535/184709971311943)
[Pull Request
Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f)

---------

Co-authored-by: Dominik Kapusta <[email protected]>
  • Loading branch information
jotaemepereira and ayoy authored Jan 3, 2025
1 parent 4c48e64 commit 5473b36
Show file tree
Hide file tree
Showing 19 changed files with 817 additions and 39 deletions.
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.
4 changes: 3 additions & 1 deletion DuckDuckGo/HomePage/View/HomePageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,9 @@ extension HomePage.Views {

@ViewBuilder
func remoteMessage() -> some View {
if let remoteMessage = activeRemoteMessageModel.remoteMessage, let modelType = remoteMessage.content, modelType.isSupported {
if let remoteMessage = activeRemoteMessageModel.newTabPageRemoteMessage,
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 @@ -21,8 +21,10 @@ import NewTabPage
import RemoteMessaging

extension ActiveRemoteMessageModel: NewTabPageActiveRemoteMessageProviding {
var remoteMessagePublisher: AnyPublisher<RemoteMessageModel?, Never> {
$remoteMessage.dropFirst().eraseToAnyPublisher()
var newTabPageRemoteMessagePublisher: AnyPublisher<RemoteMessageModel?, Never> {
$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 {
}
}
}

private extension RemoteMessageModel {

var isForTabBar: Bool {
return id == TabBarRemoteMessage.tabBarPermanentSurveyRemoteMessageId
}
}
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)
.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

0 comments on commit 5473b36

Please sign in to comment.