Merge branch 'main' of github.com:omnivore-app/omnivore into issue-835
This commit is contained in:
@ -159,7 +159,7 @@
|
||||
"location" : "https://github.com/PSPDFKit/PSPDFKit-SP",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "0e18629c443e3f39ecfee0f600d9ef5551ecf488"
|
||||
"revision" : "b7e5465ab62f5b48735145756e371502fa2a24f0"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,10 +1,3 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Jackson Harper on 6/1/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Models
|
||||
import Services
|
||||
@ -18,23 +11,25 @@ class ExtensionSaveService {
|
||||
self.queue = OperationQueue()
|
||||
}
|
||||
|
||||
private func queueSaveOperation(
|
||||
_ pageScrape: PageScrapePayload,
|
||||
shareExtensionViewModel: ShareExtensionChildViewModel
|
||||
) {
|
||||
ProcessInfo().performExpiringActivity(withReason: "app.omnivore.SaveActivity") { [self] expiring in
|
||||
guard !expiring else {
|
||||
self.queue.cancelAllOperations()
|
||||
#if os(iOS)
|
||||
private func queueSaveOperation(
|
||||
_ pageScrape: PageScrapePayload,
|
||||
shareExtensionViewModel: ShareExtensionChildViewModel
|
||||
) {
|
||||
ProcessInfo().performExpiringActivity(withReason: "app.omnivore.SaveActivity") { [self] expiring in
|
||||
guard !expiring else {
|
||||
self.queue.cancelAllOperations()
|
||||
self.queue.waitUntilAllOperationsAreFinished()
|
||||
return
|
||||
}
|
||||
|
||||
let operation = SaveOperation(pageScrapePayload: pageScrape, shareExtensionViewModel: shareExtensionViewModel)
|
||||
|
||||
self.queue.addOperation(operation)
|
||||
self.queue.waitUntilAllOperationsAreFinished()
|
||||
return
|
||||
}
|
||||
|
||||
let operation = SaveOperation(pageScrapePayload: pageScrape, shareExtensionViewModel: shareExtensionViewModel)
|
||||
|
||||
self.queue.addOperation(operation)
|
||||
self.queue.waitUntilAllOperationsAreFinished()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public func save(_ extensionContext: NSExtensionContext, shareExtensionViewModel: ShareExtensionChildViewModel) {
|
||||
PageScraper.scrape(extensionContext: extensionContext) { [weak self] result in
|
||||
@ -71,7 +66,10 @@ class ExtensionSaveService {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.queueSaveOperation(payload, shareExtensionViewModel: shareExtensionViewModel)
|
||||
#if os(iOS)
|
||||
// TODO: need alternative call for macos
|
||||
self.queueSaveOperation(payload, shareExtensionViewModel: shareExtensionViewModel)
|
||||
#endif
|
||||
case .failure:
|
||||
DispatchQueue.main.async {
|
||||
shareExtensionViewModel.status = .failed(error: .unknown(description: "Could not retrieve content"))
|
||||
|
||||
@ -66,24 +66,26 @@ struct LinkedItemTitleEditView: View {
|
||||
NavigationView {
|
||||
editForm
|
||||
.navigationTitle("Edit Title and Description")
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
Button(
|
||||
action: {
|
||||
viewModel.submit(dataService: dataService, item: item)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
},
|
||||
label: { Text("Save").foregroundColor(.appGrayTextContrast) }
|
||||
)
|
||||
}
|
||||
ToolbarItem(placement: .barLeading) {
|
||||
Button(
|
||||
action: { presentationMode.wrappedValue.dismiss() },
|
||||
label: { Text("Cancel").foregroundColor(.appGrayTextContrast) }
|
||||
)
|
||||
}
|
||||
#endif
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
Button(
|
||||
action: {
|
||||
viewModel.submit(dataService: dataService, item: item)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
},
|
||||
label: { Text("Save").foregroundColor(.appGrayTextContrast) }
|
||||
)
|
||||
}
|
||||
ToolbarItem(placement: .barLeading) {
|
||||
Button(
|
||||
action: { presentationMode.wrappedValue.dismiss() },
|
||||
label: { Text("Cancel").foregroundColor(.appGrayTextContrast) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.task { viewModel.load(item: item) }
|
||||
}
|
||||
|
||||
@ -59,8 +59,7 @@ import Views
|
||||
}
|
||||
|
||||
func handleGoogleAuth(authenticator: Authenticator) async {
|
||||
guard let presentingViewController = presentingViewController() else { return }
|
||||
let googleAuthResponse = await authenticator.handleGoogleAuth(presenting: presentingViewController)
|
||||
let googleAuthResponse = await authenticator.handleGoogleAuth()
|
||||
|
||||
switch googleAuthResponse {
|
||||
case let .loginError(error):
|
||||
@ -72,15 +71,3 @@ import Views
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentingViewController() -> PlatformViewController? {
|
||||
#if os(iOS)
|
||||
let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
|
||||
return scene?.windows
|
||||
.filter(\.isKeyWindow)
|
||||
.first?
|
||||
.rootViewController
|
||||
#elseif os(macOS)
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -139,6 +139,16 @@ import WebKit
|
||||
|
||||
func loadContent(webView: WKWebView) {
|
||||
let fontFamilyValue = UserDefaults.standard.string(forKey: UserDefaultKey.preferredWebFont.rawValue)
|
||||
|
||||
let prefersHighContrastText: Bool = {
|
||||
let key = UserDefaultKey.prefersHighContrastWebFont.rawValue
|
||||
if UserDefaults.standard.object(forKey: key) != nil {
|
||||
return UserDefaults.standard.bool(forKey: key)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}()
|
||||
|
||||
let fontFamily = fontFamilyValue.flatMap { WebFont(rawValue: $0) } ?? .inter
|
||||
|
||||
webView.loadHTMLString(
|
||||
@ -149,7 +159,8 @@ import WebKit
|
||||
fontSize: fontSize(),
|
||||
lineHeight: lineHeight(),
|
||||
maxWidthPercentage: maxWidthPercentage(),
|
||||
fontFamily: fontFamily
|
||||
fontFamily: fontFamily,
|
||||
prefersHighContrastText: prefersHighContrastText
|
||||
)
|
||||
.styledContent,
|
||||
baseURL: ViewsPackage.bundleURL
|
||||
|
||||
@ -11,6 +11,7 @@ struct WebReaderContent {
|
||||
let themeKey: String
|
||||
let fontFamily: WebFont
|
||||
let articleContent: ArticleContent
|
||||
let prefersHighContrastText: Bool
|
||||
|
||||
init(
|
||||
item: LinkedItem,
|
||||
@ -19,7 +20,8 @@ struct WebReaderContent {
|
||||
fontSize: Int,
|
||||
lineHeight: Int,
|
||||
maxWidthPercentage: Int,
|
||||
fontFamily: WebFont
|
||||
fontFamily: WebFont,
|
||||
prefersHighContrastText: Bool
|
||||
) {
|
||||
self.textFontSize = fontSize
|
||||
self.lineHeight = lineHeight
|
||||
@ -28,6 +30,7 @@ struct WebReaderContent {
|
||||
self.themeKey = isDark ? "Gray" : "LightGray"
|
||||
self.fontFamily = fontFamily
|
||||
self.articleContent = articleContent
|
||||
self.prefersHighContrastText = prefersHighContrastText
|
||||
}
|
||||
|
||||
// swiftlint:disable line_length
|
||||
@ -82,6 +85,7 @@ struct WebReaderContent {
|
||||
window.maxWidthPercentage = \(maxWidthPercentage)
|
||||
window.lineHeight = \(lineHeight)
|
||||
window.localStorage.setItem("theme", "\(themeKey)")
|
||||
window.prefersHighContrastFont = \(prefersHighContrastText)
|
||||
</script>
|
||||
<script src="bundle.js"></script>
|
||||
<script src="mathJaxConfiguration.js" id="MathJax-script"></script>
|
||||
|
||||
@ -14,9 +14,14 @@ public class PersistentContainer: NSPersistentContainer {
|
||||
|
||||
// Store the sqlite file in the app group container.
|
||||
// This allows shared access for app and app extensions.
|
||||
let appGroupID = "group.app.omnivoreapp"
|
||||
#if os(iOS)
|
||||
let appGroupID = "group.app.omnivoreapp"
|
||||
#else
|
||||
let appGroupID = "QJF2XZ86HB.app.omnivore.app"
|
||||
#endif
|
||||
let appGroupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupID)
|
||||
let appGroupContainerURL = appGroupContainer?.appendingPathComponent("store.sqlite")
|
||||
|
||||
container.persistentStoreDescriptions.first!.url = appGroupContainerURL
|
||||
|
||||
container.viewContext.automaticallyMergesChangesFromParent = true
|
||||
|
||||
@ -10,8 +10,11 @@ public enum GoogleAuthResponse {
|
||||
}
|
||||
|
||||
extension Authenticator {
|
||||
public func handleGoogleAuth(presenting: PlatformViewController) async -> GoogleAuthResponse {
|
||||
let idToken = try? await googleSignIn(presenting: presenting)
|
||||
public func handleGoogleAuth() async -> GoogleAuthResponse {
|
||||
let idToken = await withCheckedContinuation { continuation in
|
||||
googleSignIn { continuation.resume(returning: $0) }
|
||||
}
|
||||
|
||||
guard let idToken = idToken else { return .loginError(error: .unauthorized) }
|
||||
|
||||
do {
|
||||
@ -47,27 +50,47 @@ extension Authenticator {
|
||||
}
|
||||
}
|
||||
|
||||
func googleSignIn(presenting: PlatformViewController) async throws -> String {
|
||||
try await withCheckedThrowingContinuation { continuation in
|
||||
let clientID = "\(AppKeys.sharedInstance?.iosClientGoogleId ?? "").apps.googleusercontent.com"
|
||||
GIDSignIn.sharedInstance.signIn(
|
||||
with: GIDConfiguration(clientID: clientID),
|
||||
presenting: presenting
|
||||
) { user, error in
|
||||
guard let user = user, error == nil else {
|
||||
continuation.resume(throwing: LoginError.unauthorized)
|
||||
func googleSignIn(completion: @escaping (String?) -> Void) {
|
||||
#if os(iOS)
|
||||
let presenting = presentingViewController()
|
||||
#else
|
||||
let presenting = NSApplication.shared.windows.first
|
||||
#endif
|
||||
|
||||
guard let presenting = presenting else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
let clientID = "\(AppKeys.sharedInstance?.iosClientGoogleId ?? "").apps.googleusercontent.com"
|
||||
|
||||
GIDSignIn.sharedInstance.signIn(
|
||||
with: GIDConfiguration(clientID: clientID),
|
||||
presenting: presenting
|
||||
) { user, error in
|
||||
guard let user = user, error == nil else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
user.authentication.do { authentication, error in
|
||||
guard let idToken = authentication?.idToken, error == nil else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
user.authentication.do { authentication, error in
|
||||
guard let idToken = authentication?.idToken, error == nil else {
|
||||
continuation.resume(throwing: LoginError.unauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
continuation.resume(returning: idToken)
|
||||
}
|
||||
completion(idToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func presentingViewController() -> PlatformViewController? {
|
||||
#if os(iOS)
|
||||
let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene
|
||||
return scene?.windows
|
||||
.filter(\.isKeyWindow)
|
||||
.first?
|
||||
.rootViewController
|
||||
#elseif os(macOS)
|
||||
return nil
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -4,9 +4,14 @@ import Foundation
|
||||
import Models
|
||||
import OSLog
|
||||
import QuickLookThumbnailing
|
||||
import UIKit
|
||||
import Utils
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
let logger = Logger(subsystem: "app.omnivore", category: "data-service")
|
||||
|
||||
public final class DataService: ObservableObject {
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import CoreImage
|
||||
import Foundation
|
||||
import QuickLookThumbnailing
|
||||
import UIKit
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#else
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public enum PDFUtils {
|
||||
public static func copyToLocal(url: URL) throws -> String {
|
||||
@ -64,7 +68,11 @@ public enum PDFUtils {
|
||||
|
||||
public static func createThumbnailFor(inputUrl: URL) async throws -> URL? {
|
||||
let size = CGSize(width: 80, height: 80)
|
||||
let scale = await UIScreen.main.scale
|
||||
#if os(iOS)
|
||||
let scale = await UIScreen.main.scale
|
||||
#else
|
||||
let scale = NSScreen.main?.backingScaleFactor ?? 1
|
||||
#endif
|
||||
let outputUrl = thumbnailUrl(localUrl: inputUrl)
|
||||
|
||||
// Create the thumbnail request.
|
||||
|
||||
@ -129,7 +129,7 @@ public enum WebViewManager {
|
||||
}
|
||||
|
||||
func fontSize() -> Int {
|
||||
let storedSize = UserDefaults.standard.integer(forKey: "preferredWebFontSize")
|
||||
let storedSize = UserDefaults.standard.integer(forKey: UserDefaultKey.preferredWebFontSize.rawValue)
|
||||
return storedSize <= 1 ? Int(NSFont.userFont(ofSize: 16)?.pointSize ?? 16) : storedSize
|
||||
}
|
||||
|
||||
@ -168,14 +168,19 @@ public enum WebViewManager {
|
||||
context.coordinator.needsReload = false
|
||||
}
|
||||
|
||||
if annotationSaveTransactionID != context.coordinator.lastSavedAnnotationID {
|
||||
context.coordinator.lastSavedAnnotationID = annotationSaveTransactionID
|
||||
(webView as? WebView)?.dispatchEvent(.saveAnnotation(annotation: annotation))
|
||||
}
|
||||
|
||||
if sendIncreaseFontSignal {
|
||||
sendIncreaseFontSignal = false
|
||||
(webView as? WebView)?.increaseFontSize()
|
||||
(webView as? WebView)?.updateFontSize()
|
||||
}
|
||||
|
||||
if sendDecreaseFontSignal {
|
||||
sendDecreaseFontSignal = false
|
||||
(webView as? WebView)?.decreaseFontSize()
|
||||
(webView as? WebView)?.updateFontSize()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,9 +86,9 @@ public final class WebView: WKWebView {
|
||||
super.viewDidChangeEffectiveAppearance()
|
||||
switch effectiveAppearance.bestMatch(from: [.aqua, .darkAqua]) {
|
||||
case .some(.darkAqua):
|
||||
dispatchEvent("switchToDarkMode")
|
||||
dispatchEvent(.updateColorMode(isDark: true))
|
||||
default:
|
||||
dispatchEvent("switchToLightMode")
|
||||
dispatchEvent(.updateColorMode(isDark: false))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -42,7 +42,7 @@ public struct WebPreferencesPopoverView: View {
|
||||
@AppStorage(UserDefaultKey.preferredWebLineSpacing.rawValue) var storedLineSpacing = 150
|
||||
@AppStorage(UserDefaultKey.preferredWebMaxWidthPercentage.rawValue) var storedMaxWidthPercentage = 100
|
||||
@AppStorage(UserDefaultKey.preferredWebFont.rawValue) var preferredFont = WebFont.inter.rawValue
|
||||
@AppStorage(UserDefaultKey.prefersHighContrastWebFont.rawValue) var prefersHighContrastText = false
|
||||
@AppStorage(UserDefaultKey.prefersHighContrastWebFont.rawValue) var prefersHighContrastText = true
|
||||
|
||||
public init(
|
||||
updateFontFamilyAction: @escaping () -> Void,
|
||||
@ -81,7 +81,9 @@ public struct WebPreferencesPopoverView: View {
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.navigationTitle("Reader Font")
|
||||
}
|
||||
|
||||
@ -148,9 +150,11 @@ public struct WebPreferencesPopoverView: View {
|
||||
}
|
||||
.padding()
|
||||
.navigationTitle("Reader Preferences")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
Button(
|
||||
action: dismissAction,
|
||||
label: { Text("Done").foregroundColor(.appGrayTextContrast).padding() }
|
||||
@ -158,7 +162,9 @@ public struct WebPreferencesPopoverView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
#if os(iOS)
|
||||
.navigationViewStyle(.stack)
|
||||
#endif
|
||||
.accentColor(.appGrayTextContrast)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -37,36 +37,6 @@ public enum ShareExtensionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
struct CornerRadiusStyle: ViewModifier {
|
||||
var radius: CGFloat
|
||||
var corners: UIRectCorner
|
||||
|
||||
struct CornerRadiusShape: Shape {
|
||||
var radius = CGFloat.infinity
|
||||
var corners = UIRectCorner.allCorners
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let path = UIBezierPath(
|
||||
roundedRect: rect,
|
||||
byRoundingCorners: corners,
|
||||
cornerRadii: CGSize(width: radius, height: radius)
|
||||
)
|
||||
return Path(path.cgPath)
|
||||
}
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.clipShape(CornerRadiusShape(radius: radius, corners: corners))
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
||||
ModifiedContent(content: self, modifier: CornerRadiusStyle(radius: radius, corners: corners))
|
||||
}
|
||||
}
|
||||
|
||||
private extension SaveArticleError {
|
||||
var displayMessage: String {
|
||||
switch self {
|
||||
@ -197,9 +167,15 @@ public struct ShareExtensionChildView: View {
|
||||
}
|
||||
|
||||
private func localImage(from url: URL) -> Image? {
|
||||
if let data = try? Data(contentsOf: url), let img = UIImage(data: data) {
|
||||
return Image(uiImage: img)
|
||||
}
|
||||
#if os(iOS)
|
||||
if let data = try? Data(contentsOf: url), let img = UIImage(data: data) {
|
||||
return Image(uiImage: img)
|
||||
}
|
||||
#else
|
||||
if let data = try? Data(contentsOf: url), let img = NSImage(data: data) {
|
||||
return Image(nsImage: img)
|
||||
}
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@ -48,8 +48,8 @@ const appendReadFilter = (body: SearchBody, filter: ReadFilter): void => {
|
||||
case ReadFilter.UNREAD:
|
||||
body.query.bool.filter.push({
|
||||
range: {
|
||||
readingProgress: {
|
||||
gte: 98,
|
||||
readingProgressPercent: {
|
||||
lt: 98,
|
||||
},
|
||||
},
|
||||
})
|
||||
@ -57,8 +57,8 @@ const appendReadFilter = (body: SearchBody, filter: ReadFilter): void => {
|
||||
case ReadFilter.READ:
|
||||
body.query.bool.filter.push({
|
||||
range: {
|
||||
readingProgress: {
|
||||
lt: 98,
|
||||
readingProgressPercent: {
|
||||
gte: 98,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -14,7 +14,7 @@ export interface SearchBody {
|
||||
| { exists: { field: string } }
|
||||
| {
|
||||
range: {
|
||||
readingProgress: { gte: number } | { lt: number }
|
||||
readingProgressPercent: { gte: number } | { lt: number }
|
||||
}
|
||||
}
|
||||
| {
|
||||
|
||||
@ -1003,5 +1003,22 @@ describe('Article API', () => {
|
||||
expect(res.body.data.search.edges[4].node.id).to.eq(highlights[0].id)
|
||||
})
|
||||
})
|
||||
|
||||
context('when is:unread is in the query', () => {
|
||||
before(() => {
|
||||
keyword = 'search is:unread'
|
||||
})
|
||||
|
||||
it('should return unread articles in descending order', async () => {
|
||||
const res = await graphqlRequest(query, authToken).expect(200)
|
||||
|
||||
expect(res.body.data.search.edges.length).to.eq(5)
|
||||
expect(res.body.data.search.edges[0].node.id).to.eq(pages[4].id)
|
||||
expect(res.body.data.search.edges[1].node.id).to.eq(pages[3].id)
|
||||
expect(res.body.data.search.edges[2].node.id).to.eq(pages[2].id)
|
||||
expect(res.body.data.search.edges[3].node.id).to.eq(pages[1].id)
|
||||
expect(res.body.data.search.edges[4].node.id).to.eq(pages[0].id)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -44,6 +44,7 @@ const App = () => {
|
||||
margin={window.margin}
|
||||
maxWidthPercentage={window.maxWidthPercentage}
|
||||
lineHeight={window.lineHeight}
|
||||
highContrastFont={window.prefersHighContrastFont ?? true}
|
||||
articleMutations={{
|
||||
createHighlightMutation: (input) =>
|
||||
mutation('createHighlight', input),
|
||||
|
||||
@ -82,8 +82,8 @@ export function PrimaryLayout(props: PrimaryLayoutProps): JSX.Element {
|
||||
/>
|
||||
<Box
|
||||
css={{
|
||||
minHeight: '100%',
|
||||
minWidth: '100vw',
|
||||
height: '100%',
|
||||
width: '100vw',
|
||||
bg: '$grayBase',
|
||||
}}
|
||||
>
|
||||
|
||||
@ -230,6 +230,9 @@ export const lighterTheme = createTheme(ThemeId.Lighter, {})
|
||||
|
||||
// Apply global styles in here
|
||||
export const globalStyles = globalCss({
|
||||
'body': {
|
||||
backgroundColor: '$grayBase'
|
||||
},
|
||||
'*': {
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
|
||||
Reference in New Issue
Block a user