From 3dad84b6044b297e113ac6ce7b774a62928e96bb Mon Sep 17 00:00:00 2001 From: Juan Manuel Pereira Date: Thu, 22 Feb 2024 09:46:59 -0300 Subject: [PATCH] Mute/unmute tab (#2019) --- .../MutedTabIconColor.colorset/Contents.json | 38 ++++++++ .../Contents.json | 38 ++++++++ .../Audio-Mute.imageset/Audio-Mute-12.pdf | Bin 0 -> 4374 bytes .../Images/Audio-Mute.imageset/Contents.json | 12 +++ .../Images/Audio.imageset/Audio-12.pdf | Bin 0 -> 4307 bytes .../Images/Audio.imageset/Contents.json | 12 +++ .../Extensions/WKWebViewExtension.swift | 48 ++++++++++ DuckDuckGo/Common/Localizables/UserText.swift | 2 + DuckDuckGo/Localizable.xcstrings | 24 +++++ .../Model/PinnedTabsViewModel.swift | 27 ++++++ .../PinnedTabs/View/PinnedTabView.swift | 50 +++++++++-- DuckDuckGo/Tab/Model/Tab.swift | 9 ++ .../TabBar/View/Base.lproj/TabBar.storyboard | 21 +++-- .../TabBar/View/TabBarViewController.swift | 23 +++++ DuckDuckGo/TabBar/View/TabBarViewItem.swift | 83 ++++++++++++++++-- DuckDuckGo/TabBar/View/TabBarViewItem.xib | 41 ++++++--- .../PinnedTabs/PinnedTabsViewModelTests.swift | 6 +- .../TabBar/View/MockTabViewItemDelegate.swift | 13 +++ .../TabBar/View/TabBarViewItemTests.swift | 22 +++++ 19 files changed, 427 insertions(+), 42 deletions(-) create mode 100644 DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf create mode 100644 DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json diff --git a/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json new file mode 100644 index 0000000000..3fe9b59242 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/MutedTabIconColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json b/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json new file mode 100644 index 0000000000..802fa68a4c --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Colors/PinnedTabMuteStateCircleColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFE" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0x00", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Audio-Mute-12.pdf new file mode 100644 index 0000000000000000000000000000000000000000..de1f4718ed0a91177028ae95b498623cae76a1cb GIT binary patch literal 4374 zcmeH~TaOb*5QX38SM&=KJZ!t~7YQM8SCk;gZXS?O9*lQ@CA&7UO$7P%`Kmph@m>-Z zo-;!1_)J%I^{G?U-FIF+d3I!V?mDZCTD|?)DRuw8n*H|mQ?F*PU%vg=FScMjvtRn< z`|Sti7e44NO<>Kq5?++dfu1-F`-L^@et=1O< zcHD1PpVy0iBU6kcA3a)qQE!ydrajY(b$e)f`yE$UZno=we$maI^naZ#`s3%1)Z>%G zUyDumXJ_~3qwAyb?fSQ%LpI3-wYccUVb@RnRsX=Zw1OtkEH);;i!8 z)*4e*=K?0i*{GaODdtoaP>#m)#`0?QIS!EZPFWpY4z8$$nC4JRB$2v=l1r9x1ZblN zvbmVjOuch5inRq_qcuBQ85f;G3?^pFvZ_wr+MESSCObY>ZEf|bs3Ss&t{RVR(Ir+B zl7KGPn1cfHHnSk0l!H$eQxi+@&9r*f4R2l55(yC;2?f!ZB(vsPDyCjL9zw3VhQ=A* zgeNPrvb0!Iwr$$rT`EaIvo+C_wu{Ib^PDPPux%;}%;&T(!3HCy#bz=~DxJ5XHg6%hkS!@Uux46{2qsuxjDQi2 zTFk-LY-Jp*2j|7xRkbX-58b8G(0#vY@A{#`+ zxEwuFYk^cI6;mk(*J3ltmufLZ^DrT^dI|zmwCpHi!UQ7i7V!WDYLf{Zgc}yoOaY6C z%t07p$TH5?6r(_a5`vu@X#eqaI~cSj-li#~eYeZ#zK|-iJz`N)p}8iK9i;t88cCSH zie;qAVr3kqj((ATWR20og<$>%tB(MGWmquN|DxDTnW-+ z9nPeL=<_LhLkBk6dpfMcm%&$}RZ83N)Uq^NiHhi@A7!L{hzv1^uVGZ6<+WJF*MLP$ zcMABYsXK&PRMMXHu%eNh5s@`kV|}dCD9Ib7me!54p|42c22HU@pQ^-PE&Ilpae8*i zkpdo(Q5QchN9xYA6VW!mxkrxEQn!RCIk}j$ca*5$1|A3!Fts+{R+a)g12vg+OlVS; z>opC4?x&8VcX;}28#(X-SlKAk0Wnm0(TQ=5${`m(HWmI&)L`mZ7Oi42)yx$5`46{i zbj5Z>Q~Wkzvbh&ebCeB(mR8VNJ5^LiOJ=7>(w_a&wl{XtbOl4TRpdQdqa)eKXo0AV zLqxP}=kk5RLL|DjYwfL(KieDiA}VBe6PL)iBzAGwrUYu1sb!;$*)R~###v9BJ%FV4 zAbqry`ta6Q+O5=@HFWd?BvX>a#vx#D$5tZ@5XB9=ObO}w<>_!3tQ zt>7xeetH@5o?FXRC_tG!qc5MX8|LeyIJ;jptR4<8`3jtNMrga9pPl#XPU!vV3FO}0 wtSh9U;y&c7~J1!o9%qPZR`Y-eRt>1)0fZw0h>RF!2kdN literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json new file mode 100644 index 0000000000..b609317961 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Audio-Mute.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Audio-Mute-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Audio-12.pdf new file mode 100644 index 0000000000000000000000000000000000000000..635e45f8743e3360c3aa3f3fe264526a6d890242 GIT binary patch literal 4307 zcmeH~O>Y!O5QgvbEBb;2k(ll84+&WU8>}cn5IYAXl*6(fj1zm;*ysCcO`Dp=D6LlSes7d|@f`O6LH)abS8aO3bNsF6%gyb4-7OcN@4P=c8eE-!e7CKezF4hq z2JEcctUj(6-A1+;S3Y~T`lQ|}-RkX z{l6BQ=Fi6L%tzOc$ESPW1>>X1YH`zy>kc0_i)G_mTXardNnlYYW7G{`v1E`1jK%rj zRoq1YWPK{e?m?z?Aw*l$qM6pUF}diHx}GrIrfgiWYSH7|+Y)mK0biyq z+PJ8dYh5xaMSPhy+CXgk7;bF}E>GB)t8aBFj$rqYQesNtf5dXDo%O}we-A6R6fz&Z zmo|nZtHu~cWEX2FYQ(PZ@k(LmjVNGt_dhht=9lkQQEa^r24=r5qSRXJs2)5(q^RkpNtnkH1JPS+3%W{e_Qp{Hkl;%Y z<_$%!b=A&z=Shs3MQ1gFq^o0ykyQyf2dR&fKFA=&7wa9piBORUcBM5wX*8%?-sK$V zM0%P|klweb$Mi^XPBA@XUqTQ=s%aq*uo#!?I^(m2X9loC>r4{13`W~1P}VV8h6}MG zyHv7zOiGUAWSx(6GFwnJBC8!|bc!(vR3Nm@2_b_x*}@Sx@fP^;K}&T7GT25S3kUeX zH2X|%v1?)!*(6!mHsp{~Vx4y02BM?Kit)OdmLw`8U>Z00R84~+ew81*G9pTO0$;2&fHj3%TOoNDc^@IfjH-#cyOdlXW#MB?%|GHKEcZ z-rz|FprP9AAe(v-C?k>HFufuCnItu3^b{{W6bw(YDl>z#AeQAsLs$tK`&enGl4Mdj zDQ#v}4{@PO<}0E)?Ae8A-XFfjknUcaaJsGc(?#)4dLW(x{n6 z5yUZKP*&mVWKoaUnM{4)HMg0MNvO&SIYI_Vv{pN^Lq$1R>?n}KH~G?yStQ1O&L!F= zwuB&+WD?V}Gy)KOd1?y8H^E~Nm}xffj%i3Cbg=jh)ZWw8b%@ip^%LftD<0+24ph)t zK1eYHhnTA=13MEfD!?#0S%*|ikcog83(Zw`wX%vrWjGOO1IjX#AvM;irWKuG(9#FN z!i<+h4Ly}XWWFbdA#^~eM;6j}hV+FYI!eTdrH{VO2_3RWyFf@g(=K%?5QV84&@mZ? z6ttmrnp_k>p718s)DT`x`Kr=aHC(EZOx>2`GhU2g)CHa8L+q$9bO9kZ@xnVM7ox$I z=|Y3!M@Ff3a05i|O1UiMN^&&l4^caUeae-kdd6Z%@DU1zu}E#iTQZEfzzGHY)<96Z zM;j64l~fyob&M!_L3Dzpd<&tc)UPxH0`95EVe3wcX}aabaFXLmPOFWaM*CwcV>rly z3M1WbJI!+P`2RG^{HCAl?;D28xU_E&%$P3I^Yp|SW;~IPMtg_9`>@HP9xCdfyr=9^ zwxR6Kj#cL)yjS(%_EN7WZ;-lFdb&kC|Fk_l-_EyPegE9l|IS~|H-FTp{sJU_-np(! zx0<_8vr{E4H{6TvzB~A9#)atV_3elG*In-}JqJDn7;b4}CjaK{=%`N{XFGkwJ4_8X zJLX)^&^O(dW^i`7X=YG+vf|`-PnE=BUnL*Mt(l#BMS8kiuC^QXR_-w&o!I4alA+#`C@_$#667IAwl3-_EaBmv?PHt-JS)Ox30rsr;+tn->)L@RJSX zr)qW|P7ZWmLWvaRH~Mnn&@djZiUq>%8p`qD3y7282yNH%tLtvv2)#d^ zK<>lM>ZW_79$sBM>M5R`tyb_W^woOJ&aeLNgbT>sZ0GB3WyfnBn+Fe`zk2Zx7O#DG literal 0 HcmV?d00001 diff --git a/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json new file mode 100644 index 0000000000..35d4dda319 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/Images/Audio.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Audio-12.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift index a2eb4706e4..4c70d4bacc 100644 --- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift +++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift @@ -29,6 +29,12 @@ extension WKWebView { return false } + enum AudioState { + case muted + case unmuted + case notSupported + } + enum CaptureState { case none case active @@ -129,6 +135,48 @@ extension WKWebView { } #endif + func muteOrUnmute() { +#if !APPSTORE + guard self.responds(to: #selector(WKWebView._setPageMuted(_:))) else { + assertionFailure("WKWebView does not respond to selector _stopMediaCapture") + return + } + let mutedState: _WKMediaMutedState = { + guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { return [] } + return self._mediaMutedState() + }() + var newState = mutedState + + if newState == .audioMuted { + newState.remove(.audioMuted) + } else { + newState.insert(.audioMuted) + } + guard newState != mutedState else { return } + self._setPageMuted(newState) +#endif + } + + /// Returns the audio state of the WKWebView. + /// + /// - Returns: `muted` if the web view is muted + /// `unmuted` if the web view is unmuted + /// `notSupported` if the web view does not support fetching the current audio state + func audioState() -> AudioState { +#if APPSTORE + return .notSupported +#else + guard self.responds(to: #selector(WKWebView._mediaMutedState)) else { + assertionFailure("WKWebView does not respond to selector _mediaMutedState") + return .notSupported + } + + let mutedState = self._mediaMutedState() + + return mutedState.contains(.audioMuted) ? .muted : .unmuted +#endif + } + func stopMediaCapture() { guard #available(macOS 12.0, *) else { #if !APPSTORE diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index c3f8ff0f68..eee5b3f4e5 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -199,6 +199,8 @@ struct UserText { static let pinTab = NSLocalizedString("pin.tab", value: "Pin Tab", comment: "Menu item. Pin as a verb") static let unpinTab = NSLocalizedString("unpin.tab", value: "Unpin Tab", comment: "Menu item. Unpin as a verb") static let closeTab = NSLocalizedString("close.tab", value: "Close Tab", comment: "Menu item") + static let muteTab = NSLocalizedString("mute.tab", value: "Mute Tab", comment: "Menu item. Mute tab") + static let unmuteTab = NSLocalizedString("unmute.tab", value: "Unmute Tab", comment: "Menu item. Unmute tab") static let closeOtherTabs = NSLocalizedString("close.other.tabs", value: "Close Other Tabs", comment: "Menu item") static let closeTabsToTheRight = NSLocalizedString("close.tabs.to.the.right", value: "Close Tabs to the Right", comment: "Menu item") static let openInNewTab = NSLocalizedString("open.in.new.tab", value: "Open in New Tab", comment: "Menu item that opens the link in a new tab") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index faef1b6820..ddf6e62331 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -5092,6 +5092,18 @@ } } }, + "mute.tab" : { + "comment" : "Menu item. Mute tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Mute Tab" + } + } + } + }, "n.more.tabs" : { "comment" : "String in Recently Closed menu item for recently closed browser window and number of tabs contained in the closed window", "extractionState" : "extracted_with_value", @@ -9100,6 +9112,18 @@ } } }, + "unmute.tab" : { + "comment" : "Menu item. Unmute tab", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Unmute Tab" + } + } + } + }, "unpin.tab" : { "comment" : "Menu item. Unpin as a verb", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift index 2c714304df..ee5c9f0c91 100644 --- a/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift +++ b/DuckDuckGo/PinnedTabs/Model/PinnedTabsViewModel.swift @@ -46,6 +46,7 @@ final class PinnedTabsViewModel: ObservableObject { didSet { if let selectedItem = selectedItem { selectedItemIndex = items.firstIndex(of: selectedItem) + updateTabAudioState(tab: selectedItem) } else { selectedItemIndex = nil } @@ -57,6 +58,7 @@ final class PinnedTabsViewModel: ObservableObject { didSet { if let hoveredItem = hoveredItem { hoveredItemIndex = items.firstIndex(of: hoveredItem) + updateTabAudioState(tab: hoveredItem) } else { hoveredItemIndex = nil } @@ -72,6 +74,7 @@ final class PinnedTabsViewModel: ObservableObject { @Published private(set) var selectedItemIndex: Int? @Published private(set) var hoveredItemIndex: Int? @Published private(set) var dragMovesWindow: Bool = true + @Published private(set) var audioStateView: AudioStateView = .notSupported @Published private(set) var itemsWithoutSeparator: Set = [] @@ -111,6 +114,18 @@ final class PinnedTabsViewModel: ObservableObject { } itemsWithoutSeparator = items } + + private func updateTabAudioState(tab: Tab) { + let audioState = tab.audioState + switch audioState { + case .muted: + audioStateView = .muted + case .unmuted: + audioStateView = .unmuted + case .notSupported: + audioStateView = .notSupported + } + } } // MARK: - Context Menu @@ -124,6 +139,13 @@ extension PinnedTabsViewModel { case fireproof(Tab) case removeFireproofing(Tab) case close(Int) + case muteOrUnmute(Tab) + } + + enum AudioStateView { + case muted + case unmuted + case notSupported } func isFireproof(_ tab: Tab) -> Bool { @@ -168,4 +190,9 @@ extension PinnedTabsViewModel { func removeFireproofing(_ tab: Tab) { contextMenuActionSubject.send(.removeFireproofing(tab)) } + + func muteOrUmute(_ tab: Tab) { + contextMenuActionSubject.send(.muteOrUnmute(tab)) + updateTabAudioState(tab: tab) + } } diff --git a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift index 148b0d5d82..278fdfa7ca 100644 --- a/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift +++ b/DuckDuckGo/PinnedTabs/View/PinnedTabView.swift @@ -21,8 +21,8 @@ import SwiftUIExtensions struct PinnedTabView: View { enum Const { - static let dimension: CGFloat = 32 - static let cornerRadius: CGFloat = 6 + static let dimension: CGFloat = 34 + static let cornerRadius: CGFloat = 10 } @ObservedObject var model: Tab @@ -96,7 +96,17 @@ struct PinnedTabView: View { fireproofAction Divider() - + switch collectionModel.audioStateView { + case .muted, .unmuted: + let audioStateText = collectionModel.audioStateView == .muted ? UserText.unmuteTab : UserText.muteTab + Button(audioStateText) { [weak collectionModel, weak model] in + guard let model = model else { return } + collectionModel?.muteOrUmute(model) + } + Divider() + case .notSupported: + EmptyView() + } Button(UserText.closeTab) { [weak collectionModel, weak model] in guard let model = model else { return } collectionModel?.close(model) @@ -163,6 +173,7 @@ struct PinnedTabInnerView: View { var foregroundColor: Color var drawSeparator: Bool = true + @Environment(\.colorScheme) var colorScheme @EnvironmentObject var model: Tab @Environment(\.controlActiveState) private var controlActiveState @@ -187,11 +198,32 @@ struct PinnedTabInnerView: View { .frame(width: PinnedTabView.Const.dimension) } + @ViewBuilder + var mutedTabIndicator: some View { + switch model.audioState { + case .muted: + ZStack { + Circle() + .stroke(Color.gray.opacity(0.5), lineWidth: 0.5) + .background(Circle().foregroundColor(Color("PinnedTabMuteStateCircleColor"))) + .frame(width: 16, height: 16) + Image("Audio-Mute") + .resizable() + .renderingMode(.template) + .frame(width: 12, height: 12) + }.offset(x: 8, y: -8) + default: EmptyView() + } + } + @ViewBuilder var favicon: some View { if let favicon = model.favicon { - Image(nsImage: favicon) - .resizable() + ZStack(alignment: .topTrailing) { + Image(nsImage: favicon) + .resizable() + mutedTabIndicator + } } else if let domain = model.content.url?.host, let eTLDplus1 = ContentBlocking.shared.tld.eTLDplus1(domain), let firstLetter = eTLDplus1.capitalized.first.flatMap(String.init) { ZStack { Rectangle() @@ -199,11 +231,15 @@ struct PinnedTabInnerView: View { Text(firstLetter) .font(.caption) .foregroundColor(.white) + mutedTabIndicator } .cornerRadius(4.0) } else { - Image(nsImage: #imageLiteral(resourceName: "Web")) - .resizable() + ZStack { + Image(nsImage: #imageLiteral(resourceName: "Web")) + .resizable() + mutedTabIndicator + } } } } diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift index aba8f89859..5c1c11d1db 100644 --- a/DuckDuckGo/Tab/Model/Tab.swift +++ b/DuckDuckGo/Tab/Model/Tab.swift @@ -488,6 +488,7 @@ protocol NewWindowPolicyDecisionMaker { } #endif + self.audioState = webView.audioState() addDeallocationChecks(for: webView) } @@ -934,6 +935,14 @@ protocol NewWindowPolicyDecisionMaker { } } + @Published private(set) var audioState: WKWebView.AudioState = .notSupported + + func muteUnmuteTab() { + webView.muteOrUnmute() + + audioState = webView.audioState() + } + @MainActor(unsafe) @discardableResult private func reloadIfNeeded(shouldLoadInBackground: Bool = false) -> ExpectedNavigation? { diff --git a/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard b/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard index 2bf37ba9b2..ff559dfdfe 100644 --- a/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard +++ b/DuckDuckGo/TabBar/View/Base.lproj/TabBar.storyboard @@ -1,7 +1,7 @@ - + - + @@ -62,7 +62,7 @@ - + - + @@ -116,7 +125,7 @@ - + @@ -124,7 +133,9 @@ + + @@ -134,11 +145,12 @@ - - + + + @@ -147,7 +159,7 @@ - + @@ -158,17 +170,20 @@ + + + diff --git a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift index 6e34010422..c9f063d54f 100644 --- a/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift +++ b/UnitTests/PinnedTabs/PinnedTabsViewModelTests.swift @@ -149,17 +149,19 @@ class PinnedTabsViewModelTests: XCTestCase { model.fireproof(tabA) model.removeFireproofing(tabB) model.close(tabA) + model.muteOrUmute(tabB) cancellable.cancel() - XCTAssertEqual(events.count, 6) + XCTAssertEqual(events.count, 7) guard case .bookmark(tabA) = events[0], case .unpin(1) = events[1], case .duplicate(0) = events[2], case .fireproof(tabA) = events[3], case .removeFireproofing(tabB) = events[4], - case .close(0) = events[5] + case .close(0) = events[5], + case .muteOrUnmute(tabB) = events[6] else { XCTFail("Incorrect context menu action") return diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index 9d576f6550..ab577650a2 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -22,6 +22,7 @@ import Foundation class MockTabViewItemDelegate: TabBarViewItemDelegate { var hasItemsToTheRight = false + var audioState: WKWebView.AudioState = .notSupported func tabBarViewItem(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem, isMouseOver: Bool) { @@ -75,8 +76,20 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } + func tabBarViewItemAudioState(_ tabBarViewItem: TabBarViewItem) -> WKWebView.AudioState { + return audioState + } + + func tabBarViewItemMuteUnmuteSite(_ tabBarViewItem: TabBarViewItem) { + + } + func otherTabBarViewItemsState(for tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> DuckDuckGo_Privacy_Browser.OtherTabBarViewItemsState { OtherTabBarViewItemsState(hasItemsToTheLeft: false, hasItemsToTheRight: hasItemsToTheRight) } + func clear() { + self.audioState = .notSupported + } + } diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index d855470109..c4e8f4418b 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -33,6 +33,10 @@ final class TabBarViewItemTests: XCTestCase { tabBarViewItem.delegate = delegate } + override func tearDown() { + delegate.clear() + } + func testThatAllExpectedItemsAreShown() { tabBarViewItem.menuNeedsUpdate(menu) @@ -48,6 +52,24 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertEqual(menu.item(at: 9)?.title, UserText.moveTabToNewWindow) } + func testThatMuteIsShownWhenCurrentAudioStateIsUnmuted() { + delegate.audioState = .unmuted + tabBarViewItem.menuNeedsUpdate(menu) + + XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.muteTab) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + } + + func testThatUnmuteIsShownWhenCurrentAudioStateIsMuted() { + delegate.audioState = .muted + tabBarViewItem.menuNeedsUpdate(menu) + + XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.unmuteTab) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + } + func testWhenOneTabCloseThenOtherTabsItemIsDisabled() { tabBarViewItem.menuNeedsUpdate(menu)