-
Notifications
You must be signed in to change notification settings - Fork 11
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
base: main
Are you sure you want to change the base?
Changes from 16 commits
3283e6e
2a880c0
62c8426
aa916e3
3f07a6d
2cf6e71
2b74542
ecc4860
3dadbce
0499ff2
472e37e
52a86be
5aca880
ba6eadf
5e8c3b9
3fb8ec2
4914438
f8252d1
f3151be
cde71c3
d68d8a4
8b91013
d56ee4d
7d54df2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,7 +47,7 @@ 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")! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
#else | ||
URL(string: "https://staticcdn.duckduckgo.com/remotemessaging/config/v1/macos-config.json")! | ||
#endif | ||
|
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.$remoteMessage.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) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,162 @@ | ||||||||||||||||||||||||||||
// | ||||||||||||||||||||||||||||
// 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 presentPopup: Bool = false | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems unused There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed it. |
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
let model: TabBarRemoteMessage | ||||||||||||||||||||||||||||
let onClose: () -> Void | ||||||||||||||||||||||||||||
let onTap: (URL) -> Void | ||||||||||||||||||||||||||||
let onHover: () -> Void | ||||||||||||||||||||||||||||
let onHoverEnd: () -> Void | ||||||||||||||||||||||||||||
let onAppear: () -> Void | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
var body: some View { | ||||||||||||||||||||||||||||
HStack { | ||||||||||||||||||||||||||||
Button(model.buttonTitle) { | ||||||||||||||||||||||||||||
onTap(model.surveyURL) | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
.buttonStyle(DefaultActionButtonStyle( | ||||||||||||||||||||||||||||
enabled: true, | ||||||||||||||||||||||||||||
onClose: { onClose() }, | ||||||||||||||||||||||||||||
onHoverStart: { | ||||||||||||||||||||||||||||
onHover() | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
onHoverEnd: { | ||||||||||||||||||||||||||||
onHoverEnd() | ||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||
.onAppear(perform: { onAppear() }) | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could simplify this a bit by passing closures directly as arguments:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This button style was removed. |
||||||||||||||||||||||||||||
.frame(width: 147) | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How would that behave with translations? Are we fine with truncating the text? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was fixed. Now, both the button and the popover will resize accordingly. |
||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
struct TabBarRemoteMessagePopoverContent: View { | ||||||||||||||||||||||||||||
enum Constants { | ||||||||||||||||||||||||||||
static let height: CGFloat = 92 | ||||||||||||||||||||||||||||
static let width: CGFloat = 360 | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Again, would be great to test it with long message bodies. Perhaps we'd want a variable height. The icon should probably stay aligned to the top, like in other remote messages. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was fixed, now we do not have fixed a fixed height for the popover, the popover will increase in height in case it spans more lines. |
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
let model: TabBarRemoteMessage | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
var body: some View { | ||||||||||||||||||||||||||||
HStack(alignment: .center, spacing: 0) { | ||||||||||||||||||||||||||||
Image(.daxResponse) | ||||||||||||||||||||||||||||
.resizable() | ||||||||||||||||||||||||||||
.scaledToFit() | ||||||||||||||||||||||||||||
.frame(width: 72, height: 72) | ||||||||||||||||||||||||||||
.padding(.leading, 8) | ||||||||||||||||||||||||||||
.padding(.trailing, 16) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
VStack(alignment: .leading, spacing: 0) { | ||||||||||||||||||||||||||||
Text(model.popupTitle) | ||||||||||||||||||||||||||||
.font(.system(size: 13, weight: .bold)) | ||||||||||||||||||||||||||||
.padding(.bottom, 8) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Text(model.popupSubtitle) | ||||||||||||||||||||||||||||
.font(.system(size: 13, weight: .medium)) | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
.padding(.trailing, 12) | ||||||||||||||||||||||||||||
.padding([.bottom, .top], 10) | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
.frame(width: Constants.width, height: Constants.height) | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
private struct DefaultActionButtonStyle: ButtonStyle { | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
public let enabled: Bool | ||||||||||||||||||||||||||||
public let onClose: () -> Void | ||||||||||||||||||||||||||||
public let onHoverStart: () -> Void | ||||||||||||||||||||||||||||
public let onHoverEnd: () -> Void | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
public init( | ||||||||||||||||||||||||||||
enabled: Bool, | ||||||||||||||||||||||||||||
onClose: @escaping () -> Void, | ||||||||||||||||||||||||||||
onHoverStart: @escaping () -> Void = {}, | ||||||||||||||||||||||||||||
onHoverEnd: @escaping () -> Void = {} | ||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||
self.enabled = enabled | ||||||||||||||||||||||||||||
self.onClose = onClose | ||||||||||||||||||||||||||||
self.onHoverStart = onHoverStart | ||||||||||||||||||||||||||||
self.onHoverEnd = onHoverEnd | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
public func makeBody(configuration: Self.Configuration) -> some View { | ||||||||||||||||||||||||||||
ButtonContent( | ||||||||||||||||||||||||||||
configuration: configuration, | ||||||||||||||||||||||||||||
enabled: enabled, | ||||||||||||||||||||||||||||
onClose: onClose, | ||||||||||||||||||||||||||||
onHoverStart: onHoverStart, | ||||||||||||||||||||||||||||
onHoverEnd: onHoverEnd | ||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
struct ButtonContent: View { | ||||||||||||||||||||||||||||
let configuration: Configuration | ||||||||||||||||||||||||||||
let enabled: Bool | ||||||||||||||||||||||||||||
let onClose: () -> Void | ||||||||||||||||||||||||||||
let onHoverStart: () -> Void | ||||||||||||||||||||||||||||
let onHoverEnd: () -> Void | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
@State private var isHovered: Bool = false | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
var body: some View { | ||||||||||||||||||||||||||||
let enabledBackgroundColor = configuration.isPressed | ||||||||||||||||||||||||||||
? Color("PrimaryButtonPressed") | ||||||||||||||||||||||||||||
: (isHovered | ||||||||||||||||||||||||||||
? Color("PrimaryButtonHover") | ||||||||||||||||||||||||||||
: Color("PrimaryButtonRest")) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
let disabledBackgroundColor = Color.gray.opacity(0.1) | ||||||||||||||||||||||||||||
let enabledLabelColor = configuration.isPressed ? Color.white.opacity(0.8) : Color.white | ||||||||||||||||||||||||||||
let disabledLabelColor = Color.primary.opacity(0.3) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
HStack(spacing: 5) { | ||||||||||||||||||||||||||||
configuration.label | ||||||||||||||||||||||||||||
.font(.system(size: 13)) | ||||||||||||||||||||||||||||
.multilineTextAlignment(.center) | ||||||||||||||||||||||||||||
.fixedSize(horizontal: false, vertical: true) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Button(action: { onClose() }) { | ||||||||||||||||||||||||||||
Image(.close) | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
.frame(width: 16, height: 16) | ||||||||||||||||||||||||||||
.buttonStyle(PlainButtonStyle()) | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
.frame(minWidth: 44) | ||||||||||||||||||||||||||||
.padding(.top, 2.5) | ||||||||||||||||||||||||||||
.padding(.bottom, 3) | ||||||||||||||||||||||||||||
.padding(.horizontal, 7.5) | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Overall I'm not sure if the design wants fraction paddings (or even odd numbers), so perhaps it's worth checking if 2, 4, and 8 would work. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed this button style. |
||||||||||||||||||||||||||||
.background(enabled ? enabledBackgroundColor : disabledBackgroundColor) | ||||||||||||||||||||||||||||
.foregroundColor(enabled ? enabledLabelColor : disabledLabelColor) | ||||||||||||||||||||||||||||
.cornerRadius(5) | ||||||||||||||||||||||||||||
.onHover { hovering in | ||||||||||||||||||||||||||||
isHovered = hovering | ||||||||||||||||||||||||||||
if hovering { | ||||||||||||||||||||||||||||
onHoverStart() | ||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||
onHoverEnd() | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
} |
ayoy marked this conversation as resolved.
Show resolved
Hide resolved
|
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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for updating this check, but here are 2 more things:
{ $0?.isForTabBar == false || $0 == nil }
NewTabPageActiveRemoteMessageProviding
protocol also declaresremoteMessage
accessor which it taken directly fromActiveRemoteMessageModel.remoteMessage
. That one isn't filtered in any way and the tab bar message still appears on HTML NTP.What we've talked about on MM, i.e. adding 2 more variables to ActiveRemoteMessageModel called e.g.
newTabPageRemoteMessage
andtabBarRemoteMessage
that would be updated every timeremoteMessage
is updated, by filtering that one appropriately, could help solve the problem here and move the filtering logic to the model.Note that if you add these additional variables, you should update
NewTabPageActiveRemoteMessageProviding
protocol to usenewTabPageRemoteMessage
andnewTabPageRemoteMessagePublisher
but this should be straightforward.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done 👍🏼