mirror of
https://github.com/ProtonMail/ios-mail.git
synced 2026-05-15 09:50:39 +00:00
ET-3505 Dynamic type size in message body
This commit is contained in:
committed by
MargeBot
parent
b5a61b5040
commit
aa85c138d4
@@ -22,6 +22,7 @@ import proton_app_uniffi
|
||||
import SwiftUI
|
||||
|
||||
struct ConversationDetailListView: View {
|
||||
@Environment(\.dynamicTypeSize) var dynamicTypeSize
|
||||
@EnvironmentObject var toastStateStore: ToastStateStore
|
||||
@ObservedObject private var model: ConversationDetailModel
|
||||
private let mailUserSession: MailUserSession
|
||||
@@ -114,6 +115,12 @@ struct ConversationDetailListView: View {
|
||||
.padding(.bottom, messages.count - 1 == index ? 0 : -DS.Spacing.extraLarge)
|
||||
}
|
||||
}
|
||||
/*
|
||||
When dynamic type size is reduced, the web views do not shrink properly and instead they remain stretched to the size they previously occupied.
|
||||
Also, the overall scroll position of the conversation is wrong.
|
||||
The easiest - though not very elegant - way to solve this is to recreate the whole list.
|
||||
*/
|
||||
.id(dynamicTypeSize)
|
||||
.onAppear {
|
||||
if let scrollToMessage = model.scrollToMessage {
|
||||
scrollView.scrollTo(scrollToMessage, anchor: .top)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
const elements = document.querySelectorAll(`[style*="font-size"][style*="line-height"]`);
|
||||
|
||||
for (const element of elements) {
|
||||
const currentStyle = element.getAttribute('style');
|
||||
|
||||
window.webkit.messageHandlers.scaleStyle.postMessage(currentStyle)
|
||||
.then(updatedStyle => {
|
||||
if (!updatedStyle) {
|
||||
return;
|
||||
}
|
||||
|
||||
element.setAttribute("style", updatedStyle);
|
||||
}).catch(error => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2025 Proton Technologies AG
|
||||
//
|
||||
// This file is part of Proton Mail.
|
||||
//
|
||||
// Proton Mail is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import WebKit
|
||||
|
||||
final class DynamicTypeSizeMessageHandler: NSObject, WKScriptMessageHandlerWithReply {
|
||||
enum MessageName: String, CaseIterable {
|
||||
case scaleStyle
|
||||
}
|
||||
|
||||
private let scalableProperties: [String] = ["font-size", "line-height"]
|
||||
|
||||
func userContentController(
|
||||
_ userContentController: WKUserContentController,
|
||||
didReceive message: WKScriptMessage
|
||||
) async -> (Any?, String?) {
|
||||
let styleString = message.body as! String
|
||||
let processedStyleString = applyScaling(to: styleString)
|
||||
return (processedStyleString, nil)
|
||||
}
|
||||
|
||||
func applyScaling(to styleString: String) -> String {
|
||||
var styleProperties = CSSStyleCoder.decode(styleString: styleString)
|
||||
|
||||
for propertyName in scalableProperties {
|
||||
guard
|
||||
let unscaledValueString = styleProperties[propertyName],
|
||||
let match = unscaledValueString.firstMatch(of: /([[:digit:]\.]+)(?:pt|px)/),
|
||||
let unscaledNumericalValue = Double(match.output.1),
|
||||
unscaledNumericalValue != 0
|
||||
else {
|
||||
continue
|
||||
}
|
||||
|
||||
let scaledNumericalValue = UIFontMetrics.default.scaledValue(for: unscaledNumericalValue)
|
||||
let scaleFactor = scaledNumericalValue / unscaledNumericalValue
|
||||
let scaleFactorPropertyName = "--dts-\(propertyName)-scale-factor"
|
||||
|
||||
styleProperties[scaleFactorPropertyName] = "\(scaleFactor)"
|
||||
styleProperties[propertyName] = "calc(\(match.output.0) * var(\(scaleFactorPropertyName))) !important"
|
||||
}
|
||||
|
||||
styleProperties["overflow-wrap"] = "anywhere !important"
|
||||
styleProperties["text-wrap-mode"] = "wrap !important"
|
||||
|
||||
return CSSStyleCoder.encode(properties: styleProperties)
|
||||
}
|
||||
}
|
||||
|
||||
private enum CSSStyleCoder {
|
||||
static func decode(styleString: String) -> [String: String] {
|
||||
styleString.components(separatedBy: ";").reduce(into: [:]) { acc, keyValueString in
|
||||
let keyValuePair = keyValueString.components(separatedBy: ":").map { $0.trimmingCharacters(in: .whitespaces) }
|
||||
|
||||
if !keyValuePair.isEmpty {
|
||||
acc[keyValuePair[0]] = keyValuePair.last
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func encode(properties: [String: String]) -> String {
|
||||
properties.map { "\($0): \($1)" }.sorted().joined(separator: ";")
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,6 @@ extension EnvironmentValues {
|
||||
}
|
||||
|
||||
struct MessageBodyReaderView: UIViewRepresentable {
|
||||
@Environment(\.webViewPrintingRegistrar) var webViewPrintingRegistrar
|
||||
@Binding var bodyContentHeight: CGFloat
|
||||
let body: MessageBody.HTML
|
||||
let viewWidth: CGFloat
|
||||
@@ -54,12 +53,24 @@ struct MessageBodyReaderView: UIViewRepresentable {
|
||||
config.userContentController.add(context.coordinator, name: handlerName.rawValue)
|
||||
}
|
||||
|
||||
let userScripts: [AppScript] = [
|
||||
.adjustLayoutAndObserveHeight(viewWidth: viewWidth),
|
||||
.handleEmptyBody,
|
||||
for messageName in DynamicTypeSizeMessageHandler.MessageName.allCases {
|
||||
config.userContentController.addScriptMessageHandler(
|
||||
context.coordinator.dynamicTypeSizeMessageHandler,
|
||||
contentWorld: .page,
|
||||
name: messageName.rawValue
|
||||
)
|
||||
}
|
||||
|
||||
var userScripts: [AppScript] = [
|
||||
.redirectConsoleLogToAppLogger,
|
||||
.handleEmptyBody,
|
||||
.adjustLayoutAndObserveHeight(viewWidth: viewWidth),
|
||||
]
|
||||
|
||||
if context.environment.dynamicTypeSize != .large {
|
||||
userScripts.append(.dynamicTypeSize)
|
||||
}
|
||||
|
||||
userScripts
|
||||
.filter(\.isEnabled)
|
||||
.map { $0.toUserScript() }
|
||||
@@ -105,6 +116,21 @@ struct MessageBodyReaderView: UIViewRepresentable {
|
||||
height: fit-content !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media not print {
|
||||
body {
|
||||
font: -apple-system-body;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
[style*="--dts-font-size-scale-factor"] {
|
||||
--dts-font-size-scale-factor: 1 !important;
|
||||
}
|
||||
[style*="--dts-line-height-scale-factor"] {
|
||||
--dts-line-height-scale-factor: 1 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
"""
|
||||
let fixedBody = body.rawBody.replacingOccurrences(of: "</head>", with: "\(style)</head>")
|
||||
@@ -121,6 +147,7 @@ extension MessageBodyReaderView {
|
||||
|
||||
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler, WKUIDelegate {
|
||||
let parent: MessageBodyReaderView
|
||||
let dynamicTypeSizeMessageHandler = DynamicTypeSizeMessageHandler()
|
||||
var urlOpener: URLOpenerProtocol?
|
||||
private var previouslyReceivedBody: MessageBody.HTML?
|
||||
private weak var webView: WKWebView?
|
||||
@@ -291,9 +318,19 @@ extension AppScript {
|
||||
console.log = function(message) {
|
||||
window.webkit.messageHandlers.\(HandlerName.consoleLog.rawValue).postMessage(message)
|
||||
};
|
||||
|
||||
console.error = function(message) {
|
||||
window.webkit.messageHandlers.\(HandlerName.consoleLog.rawValue).postMessage(message)
|
||||
};
|
||||
""",
|
||||
isEnabled: WKWebView.inspectabilityEnabled
|
||||
)
|
||||
|
||||
fileprivate static let dynamicTypeSize: Self = {
|
||||
let scriptURL = Bundle.main.url(forResource: "DynamicTypeSize", withExtension: "js")!
|
||||
let source = try! String(contentsOf: scriptURL, encoding: .utf8)
|
||||
return .init(source: source)
|
||||
}()
|
||||
}
|
||||
|
||||
private enum HandlerName: String, CaseIterable {
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2025 Proton Technologies AG
|
||||
//
|
||||
// This file is part of Proton Mail.
|
||||
//
|
||||
// Proton Mail is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import Testing
|
||||
import WebKit
|
||||
|
||||
@testable import ProtonMail
|
||||
|
||||
@MainActor
|
||||
struct DynamicTypeSizeMessageHandlerTests {
|
||||
private let sut = DynamicTypeSizeMessageHandler()
|
||||
|
||||
@Test
|
||||
func scalesFontSizeAndLineHeightSpecifiedInAbsoluteUnitsByApplyingScaleFactor() {
|
||||
let incomingStyle = "font-size: 13px; line-height: 14px"
|
||||
|
||||
let expectedStyle =
|
||||
"--dts-font-size-scale-factor: 1.0;--dts-line-height-scale-factor: 1.0;font-size: calc(13px * var(--dts-font-size-scale-factor)) !important;line-height: calc(14px * var(--dts-line-height-scale-factor)) !important;overflow-wrap: anywhere !important;text-wrap-mode: wrap !important"
|
||||
|
||||
#expect(sut.applyScaling(to: incomingStyle) == expectedStyle)
|
||||
}
|
||||
|
||||
@Test
|
||||
func doesNotModifyPropertiesWithRelativeUnits() {
|
||||
let incomingStyle = "font-family: Roboto-Regular, Helvetica, Arial, sans-serif; font-size: 13px; color: #000000de; line-height: 1.6"
|
||||
|
||||
let expectedStyle =
|
||||
"--dts-font-size-scale-factor: 1.0;color: #000000de;font-family: Roboto-Regular, Helvetica, Arial, sans-serif;font-size: calc(13px * var(--dts-font-size-scale-factor)) !important;line-height: 1.6;overflow-wrap: anywhere !important;text-wrap-mode: wrap !important"
|
||||
|
||||
#expect(sut.applyScaling(to: incomingStyle) == expectedStyle)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user