Added additional developer settings and fixed some bugs

• Added a setting to show the string keys instead of localised values
• Added settings to test the updated groups delete all messages settings
• Updated the onboarding UI to indicate whether it's currently pointing at Testnet
• Tweaked the 'isRTL' dependency setting to be clearer that it's not an injected value
• Fixed a bug with dependency name collision between the different storage types
• Fixed a bug with attachment file path creation
pull/894/head
Morgan Pretty 7 months ago
parent a5c565cacb
commit 42721db399

@ -62,7 +62,7 @@ final class VoiceMessageRecordingView: UIView {
private lazy var chevronImageView: UIImageView = {
let result: UIImageView = UIImageView(
image: (Dependencies.isRTL ?
image: (Dependencies.unsafeNonInjected.isRTL ?
UIImage(named: "small_chevron_left")?.withHorizontallyFlippedOrientation() :
UIImage(named: "small_chevron_left")
)?
@ -276,9 +276,9 @@ final class VoiceMessageRecordingView: UIView {
// MARK: - Interaction
func handleLongPressMoved(to location: CGPoint) {
if ((!Dependencies.isRTL && location.x < bounds.center.x) || (Dependencies.isRTL && location.x > bounds.center.x)) {
if ((!Dependencies.unsafeNonInjected.isRTL && location.x < bounds.center.x) || (Dependencies.unsafeNonInjected.isRTL && location.x > bounds.center.x)) {
let translationX = location.x - bounds.center.x
let sign: CGFloat = (Dependencies.isRTL ? 1 : -1)
let sign: CGFloat = (Dependencies.unsafeNonInjected.isRTL ? 1 : -1)
let chevronDamping: CGFloat = 4
let labelDamping: CGFloat = 3
let chevronX = (chevronDamping * (sqrt(abs(translationX)) / sqrt(chevronDamping))) * sign

@ -364,7 +364,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
// Flip horizontally for RTL languages
replyIconImageView.transform = CGAffineTransform.identity
.scaledBy(
x: (Dependencies.isRTL ? -1 : 1),
x: (Dependencies.unsafeNonInjected.isRTL ? -1 : 1),
y: 1
)
@ -802,7 +802,10 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
let v = panGestureRecognizer.velocity(in: self)
// Only allow swipes to the left; allowing swipes to the right gets in the way of
// the default iOS swipe to go back gesture
guard (Dependencies.isRTL && v.x > 0) || (!Dependencies.isRTL && v.x < 0) else { return false }
guard
(Dependencies.unsafeNonInjected.isRTL && v.x > 0) ||
(!Dependencies.unsafeNonInjected.isRTL && v.x < 0)
else { return false }
return abs(v.x) > abs(v.y) // It has to be more horizontal than vertical
}
@ -938,8 +941,8 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
.translation(in: self)
.x
.clamp(
(Dependencies.isRTL ? 0 : -CGFloat.greatestFiniteMagnitude),
(Dependencies.isRTL ? CGFloat.greatestFiniteMagnitude : 0)
(Dependencies.unsafeNonInjected.isRTL ? 0 : -CGFloat.greatestFiniteMagnitude),
(Dependencies.unsafeNonInjected.isRTL ? CGFloat.greatestFiniteMagnitude : 0)
)
switch gestureRecognizer.state {
@ -948,7 +951,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
case .changed:
// The idea here is to asymptotically approach a maximum drag distance
let damping: CGFloat = 20
let sign: CGFloat = (Dependencies.isRTL ? 1 : -1)
let sign: CGFloat = (Dependencies.unsafeNonInjected.isRTL ? 1 : -1)
let x = (damping * (sqrt(abs(translationX)) / sqrt(damping))) * sign
viewsToMoveForReply.forEach { $0.transform = CGAffineTransform(translationX: x, y: 0) }
@ -1203,7 +1206,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
// we only highlight those cases)
normalizedBody
.ranges(
of: (Dependencies.isRTL ?
of: (Dependencies.unsafeNonInjected.isRTL ?
"(\(part.lowercased()))(^|[^a-zA-Z0-9])" :
"(^|[^a-zA-Z0-9])(\(part.lowercased()))"
),

@ -60,6 +60,15 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob
public enum Section: SessionTableSection {
case conversationInfo
case content
case adminActions
case destructiveActions
public var style: SessionTableSectionStyle {
switch self {
case .destructiveActions: return .padding
default: return .none
}
}
}
public enum TableItem: Differentiable {
@ -81,6 +90,9 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob
case notificationMentionsOnly
case notificationMute
case blockUser
case debugDeleteBeforeNow
case debugDeleteAttachmentsBeforeNow
}
// MARK: - Content
@ -693,10 +705,71 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob
)
].compactMap { $0 }
)
let adminActionsSection: SectionModel? = nil
let destructiveActionsSection: SectionModel?
if dependencies[feature: .updatedGroupsDeleteBeforeNow] || dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow] {
destructiveActionsSection = SectionModel(
model: .destructiveActions,
elements: [
// FIXME: [GROUPS REBUILD] Need to build this properly in a future release
(!dependencies[feature: .updatedGroupsDeleteBeforeNow] || threadViewModel.threadVariant != .group ? nil :
SessionCell.Info(
id: .debugDeleteBeforeNow,
leadingAccessory: .icon(
UIImage(named: "ic_bin")?
.withRenderingMode(.alwaysTemplate),
customTint: .danger
),
title: "[DEBUG] Delete all messages before now", // stringlint:disable
styling: SessionCell.StyleInfo(
tintColor: .danger
),
confirmationInfo: ConfirmationModal.Info(
title: "delete".localized(),
body: .text("Are you sure you want to delete all messages sent before now for all group members?"), // stringlint:disable
confirmTitle: "delete".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text
),
onTap: { [weak self] in self?.deleteAllMessagesBeforeNow() }
)
),
// FIXME: [GROUPS REBUILD] Need to build this properly in a future release
(!dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow] || threadViewModel.threadVariant != .group ? nil :
SessionCell.Info(
id: .debugDeleteAttachmentsBeforeNow,
leadingAccessory: .icon(
UIImage(named: "ic_bin")?
.withRenderingMode(.alwaysTemplate),
customTint: .danger
),
title: "[DEBUG] Delete all arrachments before now", // stringlint:disable
styling: SessionCell.StyleInfo(
tintColor: .danger
),
confirmationInfo: ConfirmationModal.Info(
title: "delete".localized(),
body: .text("Are you sure you want to delete all attachments (and their associated messages) sent before now for all group members?"), // stringlint:disable
confirmTitle: "delete".localized(),
confirmStyle: .danger,
cancelStyle: .alert_text
),
onTap: { [weak self] in self?.deleteAllAttachmentsBeforeNow() }
)
)
].compactMap { $0 }
)
}
else {
destructiveActionsSection = nil
}
return [
conversationInfoSection,
standardActionsSection
standardActionsSection,
adminActionsSection,
destructiveActionsSection
].compactMap { $0 }
}
@ -1341,4 +1414,30 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigatableStateHolder, Ob
)
}
}
private func deleteAllMessagesBeforeNow() {
guard threadVariant == .group else { return }
dependencies[singleton: .storage].writeAsync { [threadId, dependencies] db in
try LibSession.deleteMessagesBefore(
db,
groupSessionId: SessionId(.group, hex: threadId),
timestamp: dependencies.dateNow.timeIntervalSince1970,
using: dependencies
)
}
}
private func deleteAllAttachmentsBeforeNow() {
guard threadVariant == .group else { return }
dependencies[singleton: .storage].writeAsync { [threadId, dependencies] db in
try LibSession.deleteAttachmentsBefore(
db,
groupSessionId: SessionId(.group, hex: threadId),
timestamp: dependencies.dateNow.timeIntervalSince1970,
using: dependencies
)
}
}
}

@ -579,10 +579,13 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi
threadVariant: .contact,
displayPictureFilename: nil,
profile: userProfile,
profileIcon: (viewModel.dependencies[feature: .serviceNetwork] == .testnet ?
.letter("T") : // stringlint:disable
.none
),
profileIcon: {
switch (viewModel.dependencies[feature: .serviceNetwork], viewModel.dependencies[feature: .forceOffline]) {
case (.testnet, false): return .letter("T", false) // stringlint:disable
case (.testnet, true): return .letter("T", true) // stringlint:disable
default: return .none
}
}(),
additionalProfile: nil,
using: viewModel.dependencies
)
@ -612,10 +615,13 @@ public final class HomeVC: BaseVC, LibSessionRespondingViewController, UITableVi
threadVariant: .contact,
displayPictureFilename: nil,
profile: userProfile,
profileIcon: (value == .testnet ?
.letter("T") : // stringlint:disable
.none
),
profileIcon: {
switch (dependencies[feature: .serviceNetwork], dependencies[feature: .forceOffline]) {
case (.testnet, false): return .letter("T", false) // stringlint:disable
case (.testnet, true): return .letter("T", true) // stringlint:disable
default: return .none
}
}(),
additionalProfile: nil,
using: dependencies
)

@ -647,7 +647,7 @@ private class DoneButton: UIView {
private lazy var chevron: UIView = {
let image: UIImage = {
guard Dependencies.isRTL else { return #imageLiteral(resourceName: "small_chevron_right") }
guard Dependencies.unsafeNonInjected.isRTL else { return #imageLiteral(resourceName: "small_chevron_right") }
return #imageLiteral(resourceName: "small_chevron_left")
}()

@ -642,7 +642,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
let rootViewControllerSetupComplete: (UIViewController) -> () = { [weak self, dependencies] rootViewController in
/// `MainAppContext.determineDeviceRTL` uses UIKit to retrime `isRTL` so must be run on the main thread to prevent
/// lag/crashes on background threads
Dependencies.setIsRTLRetriever(requiresMainThread: true) { MainAppContext.determineDeviceRTL() }
dependencies.setIsRTLRetriever(requiresMainThread: true) { MainAppContext.determineDeviceRTL() }
/// Setup the `TopBannerController`
let presentedViewController: UIViewController? = self?.window?.rootViewController?.presentedViewController
@ -706,7 +706,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
let viewController = SessionHostingViewController(rootView: LandingScreen(using: dependencies) { [weak self] in
self?.handleActivation()
})
viewController.setUpNavBarSessionIcon()
viewController.setUpNavBarSessionIcon(using: dependencies)
longRunningStartupTimoutCancellable.cancel()
rootViewControllerSetupComplete(viewController)
}
@ -714,7 +714,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
case .missingName:
DispatchQueue.main.async { [dependencies] in
let viewController = SessionHostingViewController(rootView: DisplayNameScreen(using: dependencies))
viewController.setUpNavBarSessionIcon()
viewController.setUpNavBarSessionIcon(using: dependencies)
longRunningStartupTimoutCancellable.cancel()
rootViewControllerSetupComplete(viewController)

@ -142,7 +142,7 @@ struct DisplayNameScreen: View {
let viewController: SessionHostingViewController = SessionHostingViewController(
rootView: PNModeScreen(using: dependencies)
)
viewController.setUpNavBarSessionIcon()
viewController.setUpNavBarSessionIcon(using: dependencies)
self.host.controller?.navigationController?.pushViewController(viewController, animated: true)
}
}

@ -163,7 +163,7 @@ struct LandingScreen: View {
let viewController: SessionHostingViewController = SessionHostingViewController(
rootView: DisplayNameScreen(using: viewModel.dependencies)
)
viewController.setUpNavBarSessionIcon()
viewController.setUpNavBarSessionIcon(using: viewModel.dependencies)
self.host.controller?.navigationController?.pushViewController(viewController, animated: true)
}
}

@ -75,7 +75,7 @@ struct LoadAccountScreen: View {
let viewController: SessionHostingViewController = SessionHostingViewController(
rootView: PNModeScreen(using: dependencies)
)
viewController.setUpNavBarSessionIcon()
viewController.setUpNavBarSessionIcon(using: dependencies)
self.host.controller?.navigationController?.pushViewController(viewController, animated: true)
}

@ -127,11 +127,12 @@ struct LoadingScreen: View {
let viewController: SessionHostingViewController = SessionHostingViewController(
rootView: DisplayNameScreen(using: viewModel.dependencies)
)
viewController.setUpNavBarSessionIcon()
viewController.setUpNavBarSessionIcon(using: viewModel.dependencies)
if let navigationController = self.host.controller?.navigationController {
let index = navigationController.viewControllers.count - 1
navigationController.pushViewController(viewController, animated: true)
navigationController.viewControllers.remove(at: index)
let updatedViewControllers: [UIViewController] = navigationController.viewControllers
.filter { !$0.isKind(of: SessionHostingViewController<LoadingScreen>.self) }
.appending(viewController)
navigationController.setViewControllers(updatedViewControllers, animated: true)
}
return
}

@ -145,7 +145,7 @@ struct PNModeScreen: View {
let viewController: SessionHostingViewController = SessionHostingViewController(
rootView: LoadingScreen(using: dependencies)
)
viewController.setUpNavBarSessionIcon()
viewController.setUpNavBarSessionIcon(using: dependencies)
self.host.controller?.navigationController?.pushViewController(viewController, animated: true)
}

@ -108,7 +108,7 @@ final class AppearanceViewController: BaseVC {
trailing: Values.largeSpacing
)
if Dependencies.isRTL {
if Dependencies.unsafeNonInjected.isRTL {
result.transform = CGAffineTransform.identity.scaledBy(x: -1, y: 1)
}

@ -30,6 +30,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
public enum Section: SessionTableSection {
case developerMode
case general
case logging
case network
case disappearingMessages
@ -39,6 +40,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
var title: String? {
switch self {
case .developerMode: return nil
case .general: return "General"
case .logging: return "Logging"
case .network: return "Network"
case .disappearingMessages: return "Disappearing Messages"
@ -58,11 +60,14 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
public enum TableItem: Hashable, Differentiable, CaseIterable {
case developerMode
case showStringKeys
case defaultLogLevel
case advancedLogging
case loggingCategory(String)
case serviceNetwork
case forceOffline
case resetSnodeCache
case updatedDisappearingMessages
@ -76,6 +81,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
case updatedGroupsAllowDescriptionEditing
case updatedGroupsAllowPromotions
case updatedGroupsAllowInviteById
case updatedGroupsDeleteBeforeNow
case updatedGroupsDeleteAttachmentsBeforeNow
case exportDatabase
@ -86,11 +93,14 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
public var differenceIdentifier: String {
switch self {
case .developerMode: return "developerMode"
case .showStringKeys: return "showStringKeys"
case .defaultLogLevel: return "defaultLogLevel"
case .advancedLogging: return "advancedLogging"
case .loggingCategory(let categoryIdentifier): return "loggingCategory-\(categoryIdentifier)"
case .serviceNetwork: return "serviceNetwork"
case .forceOffline: return "forceOffline"
case .resetSnodeCache: return "resetSnodeCache"
case .updatedDisappearingMessages: return "updatedDisappearingMessages"
@ -104,6 +114,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
case .updatedGroupsAllowDescriptionEditing: return "updatedGroupsAllowDescriptionEditing"
case .updatedGroupsAllowPromotions: return "updatedGroupsAllowPromotions"
case .updatedGroupsAllowInviteById: return "updatedGroupsAllowInviteById"
case .updatedGroupsDeleteBeforeNow: return "updatedGroupsDeleteBeforeNow"
case .updatedGroupsDeleteAttachmentsBeforeNow: return "updatedGroupsDeleteAttachmentsBeforeNow"
case .exportDatabase: return "exportDatabase"
}
@ -117,11 +129,14 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
var result: [TableItem] = []
switch TableItem.developerMode {
case .developerMode: result.append(.developerMode); fallthrough
case .showStringKeys: result.append(.showStringKeys); fallthrough
case .defaultLogLevel: result.append(.defaultLogLevel); fallthrough
case .advancedLogging: result.append(.advancedLogging); fallthrough
case .loggingCategory: result.append(.loggingCategory("")); fallthrough
case .serviceNetwork: result.append(.serviceNetwork); fallthrough
case .forceOffline: result.append(.forceOffline); fallthrough
case .resetSnodeCache: result.append(.resetSnodeCache); fallthrough
case .updatedDisappearingMessages: result.append(.updatedDisappearingMessages); fallthrough
@ -136,6 +151,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
case .updatedGroupsAllowDescriptionEditing: result.append(.updatedGroupsAllowDescriptionEditing); fallthrough
case .updatedGroupsAllowPromotions: result.append(.updatedGroupsAllowPromotions); fallthrough
case .updatedGroupsAllowInviteById: result.append(.updatedGroupsAllowInviteById); fallthrough
case .updatedGroupsDeleteBeforeNow: result.append(.updatedGroupsDeleteBeforeNow); fallthrough
case .updatedGroupsDeleteAttachmentsBeforeNow: result.append(.updatedGroupsDeleteAttachmentsBeforeNow); fallthrough
case .exportDatabase: result.append(.exportDatabase)
}
@ -149,11 +166,14 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
private struct State: Equatable {
let developerMode: Bool
let showStringKeys: Bool
let defaultLogLevel: Log.Level
let advancedLogging: Bool
let loggingCategories: [Log.Category: Log.Level]
let serviceNetwork: ServiceNetwork
let forceOffline: Bool
let debugDisappearingMessageDurations: Bool
let updatedDisappearingMessages: Bool
@ -166,6 +186,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
let updatedGroupsAllowDescriptionEditing: Bool
let updatedGroupsAllowPromotions: Bool
let updatedGroupsAllowInviteById: Bool
let updatedGroupsDeleteBeforeNow: Bool
let updatedGroupsDeleteAttachmentsBeforeNow: Bool
}
let title: String = "Developer Settings"
@ -174,12 +196,14 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
.refreshableData(self) { [weak self, dependencies] () -> State in
State(
developerMode: dependencies[singleton: .storage, key: .developerModeEnabled],
showStringKeys: dependencies[feature: .showStringKeys],
defaultLogLevel: dependencies[feature: .logLevel(cat: .default)],
advancedLogging: (self?.showAdvancedLogging == true),
loggingCategories: dependencies[feature: .allLogLevels].currentValues(using: dependencies),
serviceNetwork: dependencies[feature: .serviceNetwork],
forceOffline: dependencies[feature: .forceOffline],
debugDisappearingMessageDurations: dependencies[feature: .debugDisappearingMessageDurations],
updatedDisappearingMessages: dependencies[feature: .updatedDisappearingMessages],
@ -191,7 +215,9 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture],
updatedGroupsAllowDescriptionEditing: dependencies[feature: .updatedGroupsAllowDescriptionEditing],
updatedGroupsAllowPromotions: dependencies[feature: .updatedGroupsAllowPromotions],
updatedGroupsAllowInviteById: dependencies[feature: .updatedGroupsAllowInviteById]
updatedGroupsAllowInviteById: dependencies[feature: .updatedGroupsAllowInviteById],
updatedGroupsDeleteBeforeNow: dependencies[feature: .updatedGroupsDeleteBeforeNow],
updatedGroupsDeleteAttachmentsBeforeNow: dependencies[feature: .updatedGroupsDeleteAttachmentsBeforeNow]
)
}
.compactMapWithPrevious { [weak self] prev, current -> [SectionModel]? in self?.content(prev, current) }
@ -223,6 +249,32 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
)
]
),
SectionModel(
model: .general,
elements: [
SessionCell.Info(
id: .showStringKeys,
title: "Show String Keys",
subtitle: """
Controls whether localised strings should render using their keys rather than the localised value (strings will be rendered as "[{key}]")
Notes:
This change will only apply to newly created screens (eg. the Settings screen will need to be closed and reopened before it gets updated
The "Home" screen won't update as it never gets recreated
""",
trailingAccessory: .toggle(
current.showStringKeys,
oldValue: previous?.showStringKeys
),
onTap: { [weak self] in
self?.updateFlag(
for: .showStringKeys,
to: !current.showStringKeys
)
}
)
]
),
SectionModel(
model: .logging,
elements: [
@ -304,7 +356,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
The environment used for sending requests and storing messages.
<b>Warning:</b>
Changing this setting will result in all conversation and snode data being cleared and any pending network requests being cancelled.
Changing between some of these options can result in all conversation and snode data being cleared and any pending network requests being cancelled.
""",
trailingAccessory: .dropDown { current.serviceNetwork.title },
onTap: { [weak self, dependencies] in
@ -323,6 +375,18 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
)
}
),
SessionCell.Info(
id: .forceOffline,
title: "Force Offline",
subtitle: """
Shut down the current network and cause all future network requests to fail after a 1 second delay with a 'serviceUnavailable' error.
""",
trailingAccessory: .toggle(
current.forceOffline,
oldValue: previous?.forceOffline
),
onTap: { [weak self] in self?.updateForceOffline(current: current.forceOffline) }
),
SessionCell.Info(
id: .resetSnodeCache,
title: "Reset Service Node Cache",
@ -513,7 +577,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
id: .updatedGroupsAllowInviteById,
title: "Allow Invite by ID",
subtitle: """
Controls whether the UI allows group admins to invlide other group members directly by their Account ID.
Controls whether the UI allows group admins to invite other group members directly by their Account ID.
<b>Note:</b> In a future release we will offer this functionality but it's not included in the initial release.
""",
@ -527,6 +591,44 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
to: !current.updatedGroupsAllowInviteById
)
}
),
SessionCell.Info(
id: .updatedGroupsDeleteBeforeNow,
title: "Show button to delete messages before now",
subtitle: """
Controls whether the UI allows group admins to delete all messages in the group that were sent before the button was pressed.
<b>Note:</b> In a future release we will offer this functionality but it's not included in the initial release.
""",
trailingAccessory: .toggle(
current.updatedGroupsDeleteBeforeNow,
oldValue: previous?.updatedGroupsDeleteBeforeNow
),
onTap: { [weak self] in
self?.updateFlag(
for: .updatedGroupsDeleteBeforeNow,
to: !current.updatedGroupsDeleteBeforeNow
)
}
),
SessionCell.Info(
id: .updatedGroupsDeleteAttachmentsBeforeNow,
title: "Show button to delete attachments before now",
subtitle: """
Controls whether the UI allows group admins to delete all attachments (and their associated messages) in the group that were sent before the button was pressed.
<b>Note:</b> In a future release we will offer this functionality but it's not included in the initial release.
""",
trailingAccessory: .toggle(
current.updatedGroupsDeleteAttachmentsBeforeNow,
oldValue: previous?.updatedGroupsDeleteAttachmentsBeforeNow
),
onTap: { [weak self] in
self?.updateFlag(
for: .updatedGroupsDeleteAttachmentsBeforeNow,
to: !current.updatedGroupsDeleteAttachmentsBeforeNow
)
}
)
]
),
@ -559,6 +661,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
TableItem.allCases.forEach { item in
switch item {
case .developerMode: break // Not a feature
case .showStringKeys: updateFlag(for: .showStringKeys, to: nil)
case .resetSnodeCache: break // Not a feature
case .exportDatabase: break // Not a feature
case .advancedLogging: break // Not a feature
@ -567,6 +671,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
case .loggingCategory: resetLoggingCategories()
case .serviceNetwork: updateServiceNetwork(to: nil)
case .forceOffline: updateFlag(for: .forceOffline, to: nil)
case .debugDisappearingMessageDurations:
updateFlag(for: .debugDisappearingMessageDurations, to: nil)
@ -582,6 +687,8 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
updateFlag(for: .updatedGroupsAllowDescriptionEditing, to: nil)
case .updatedGroupsAllowPromotions: updateFlag(for: .updatedGroupsAllowPromotions, to: nil)
case .updatedGroupsAllowInviteById: updateFlag(for: .updatedGroupsAllowInviteById, to: nil)
case .updatedGroupsDeleteBeforeNow: updateFlag(for: .updatedGroupsDeleteBeforeNow, to: nil)
case .updatedGroupsDeleteAttachmentsBeforeNow: updateFlag(for: .updatedGroupsDeleteAttachmentsBeforeNow, to: nil)
}
}
@ -738,6 +845,17 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
forceRefresh(type: .databaseQuery)
}
private func updateForceOffline(current: Bool) {
updateFlag(for: .forceOffline, to: !current)
// Reset the network cache
dependencies.mutate(cache: .libSessionNetwork) {
$0.setPaths(paths: [])
$0.setNetworkStatus(status: current ? .unknown : .disconnected)
}
dependencies.remove(cache: .libSessionNetwork)
}
private func resetServiceNodeCache() {
self.transitionToScreen(
ConfirmationModal(
@ -894,5 +1012,9 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
// MARK: - Listable Conformance
extension ServiceNetwork: Listable {}
extension ServiceNetwork: @retroactive ContentIdentifiable {}
extension ServiceNetwork: @retroactive ContentEquatable {}
extension ServiceNetwork: @retroactive Listable {}
extension Log.Level: @retroactive ContentIdentifiable {}
extension Log.Level: @retroactive ContentEquatable {}
extension Log.Level: Listable {}

@ -143,9 +143,13 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl
id: state.profile.id,
size: .hero,
profile: state.profile,
profileIcon: (dependencies[feature: .serviceNetwork] == .mainnet ? .none :
.letter("T") // stringlint:disable
)
profileIcon: {
switch (dependencies[feature: .serviceNetwork], dependencies[feature: .forceOffline]) {
case (.testnet, false): return .letter("T", false) // stringlint:disable
case (.testnet, true): return .letter("T", true) // stringlint:disable
default: return .none
}
}()
),
styling: SessionCell.StyleInfo(
alignment: .centerHugging,

@ -69,14 +69,4 @@ public class BaseVC: UIViewController {
navigationItem.titleView = headingImageView
}
internal func setUpNavBarSessionIcon() {
let logoImageView = UIImageView()
logoImageView.image = #imageLiteral(resourceName: "SessionGreen32")
logoImageView.contentMode = .scaleAspectFit
logoImageView.set(.width, to: 32)
logoImageView.set(.height, to: 32)
navigationItem.titleView = logoImageView
}
}

@ -741,7 +741,7 @@ public final class FullConversationCell: UITableViewCell, SwipeActionOptimisticC
// with the term so we use the regex below to ensure we only highlight those cases)
normalizedSnippet
.ranges(
of: (Dependencies.isRTL ?
of: (Dependencies.unsafeNonInjected.isRTL ?
"(\(part.lowercased()))(^|[^a-zA-Z0-9])" : // stringlint:disable
"(^|[^a-zA-Z0-9])(\(part.lowercased()))" // stringlint:disable
),

@ -111,14 +111,70 @@ public class SessionHostingViewController<Content>: UIHostingController<Modified
navigationItem.titleView = headingImageView
}
internal func setUpNavBarSessionIcon() {
internal func setUpNavBarSessionIcon(using dependencies: Dependencies) {
let logoImageView = UIImageView()
logoImageView.image = #imageLiteral(resourceName: "SessionGreen32")
logoImageView.contentMode = .scaleAspectFit
logoImageView.set(.width, to: 32)
logoImageView.set(.height, to: 32)
navigationItem.titleView = logoImageView
switch (dependencies[feature: .serviceNetwork], dependencies[feature: .forceOffline]) {
case (.mainnet, false): navigationItem.titleView = logoImageView
case (.testnet, _), (.mainnet, true):
let containerView: UIView = UIView()
containerView.clipsToBounds = false
containerView.addSubview(logoImageView)
logoImageView.pin(to: containerView)
let labelStackView: UIStackView = UIStackView()
labelStackView.axis = .vertical
containerView.addSubview(labelStackView)
labelStackView.center(in: containerView)
labelStackView.transform = CGAffineTransform.identity.rotated(by: -(CGFloat.pi / 6))
let testnetLabel: UILabel = UILabel()
testnetLabel.font = Fonts.boldSpaceMono(ofSize: 14)
testnetLabel.textAlignment = .center
if dependencies[feature: .serviceNetwork] != .mainnet {
labelStackView.addArrangedSubview(testnetLabel)
}
let offlineLabel: UILabel = UILabel()
offlineLabel.font = Fonts.boldSpaceMono(ofSize: 14)
offlineLabel.textAlignment = .center
labelStackView.addArrangedSubview(offlineLabel)
ThemeManager.onThemeChange(observer: testnetLabel) { [weak testnetLabel, weak offlineLabel] theme, primaryColor in
guard
let textColor: UIColor = theme.color(for: .textPrimary),
let strokeColor: UIColor = theme.color(for: .backgroundPrimary)
else { return }
if dependencies[feature: .serviceNetwork] != .mainnet {
testnetLabel?.attributedText = NSAttributedString(
string: dependencies[feature: .serviceNetwork].title,
attributes: [
.foregroundColor: textColor,
.strokeColor: strokeColor,
.strokeWidth: -3
]
)
}
offlineLabel?.attributedText = NSAttributedString(
string: "Offline", // stringlint:disable
attributes: [
.foregroundColor: textColor,
.strokeColor: strokeColor,
.strokeWidth: -3
]
)
}
navigationItem.titleView = containerView
}
}
internal func setUpDismissingButton(on postion: NavigationItemPosition) {

@ -615,10 +615,6 @@ extension Attachment {
}
public static func originalFilePath(id: String, mimeType: String, sourceFilename: String?, using dependencies: Dependencies) -> String? {
// Store the file in a subdirectory whose name is the uniqueId of this attachment,
// to avoid collisions between multiple attachments with the same name
let attachmentFolder: String = Attachment.attachmentsFolder(using: dependencies).appending("/\(id)")
if let sourceFilename: String = sourceFilename, !sourceFilename.isEmpty {
// Ensure that the filename is a valid filesystem name,
// replacing invalid characters with an underscore.
@ -652,6 +648,10 @@ extension Attachment {
targetFileExtension = targetFileExtension.lowercased()
if !targetFileExtension.isEmpty {
// Store the file in a subdirectory whose name is the uniqueId of this attachment,
// to avoid collisions between multiple attachments with the same name
let attachmentFolder: String = Attachment.attachmentsFolder(using: dependencies).appending("/\(id)")
guard case .success = Result(try FileSystem.ensureDirectoryExists(at: attachmentFolder, using: dependencies)) else {
return nil
}
@ -665,7 +665,7 @@ extension Attachment {
UTType.fileExtensionDefault
).lowercased()
return attachmentFolder.appending("/\(id).\(targetFileExtension)")
return Attachment.attachmentsFolder(using: dependencies).appending("/\(id).\(targetFileExtension)")
}
public static func localRelativeFilePath(from originalFilePath: String?, using dependencies: Dependencies) -> String? {

@ -34,6 +34,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
self.contentHandler = contentHandler
self.request = request
/// Create a new `Dependencies` instance each time so we don't need to worry about state from previous
/// notifications causing issues with new notifications
self.dependencies = Dependencies.createEmpty()
// It's technically possible for 'completeSilently' to be called twice due to the NSE timeout so
self.hasCompleted.mutate { $0 = false }
@ -51,15 +55,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
/// Create the context if we don't have it (needed before _any_ interaction with the database)
if !dependencies[singleton: .appContext].isValid {
dependencies.set(singleton: .appContext, to: NotificationServiceExtensionContext(using: dependencies))
Dependencies.setIsRTLRetriever(requiresMainThread: false) {
dependencies.setIsRTLRetriever(requiresMainThread: false) {
NotificationServiceExtensionContext.determineDeviceRTL()
}
}
/// Perform main setup (create a new `Dependencies` instance each time so we don't need to worry about state from previous
/// notifications causing issues with new notifications
self.dependencies = Dependencies.createEmpty()
/// Actually perform the setup
DispatchQueue.main.sync {
self.performSetup { [weak self] in
self?.handleNotification(notificationContent, isPerformingResetup: false)

@ -37,7 +37,7 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
/// to override it results in the share context crashing so ensure it doesn't exist first)
if !dependencies[singleton: .appContext].isValid {
dependencies.set(singleton: .appContext, to: ShareAppExtensionContext(rootViewController: self, using: dependencies))
Dependencies.setIsRTLRetriever(requiresMainThread: false) { ShareAppExtensionContext.determineDeviceRTL() }
dependencies.setIsRTLRetriever(requiresMainThread: false) { ShareAppExtensionContext.determineDeviceRTL() }
}
guard !SNUtilitiesKit.isRunningTests else { return }

@ -664,6 +664,14 @@ public extension LibSession {
// Create the network object
getOrCreateNetwork().sinkUntilComplete()
// If the app has been set to 'forceOffline' then we need to explicitly set the network status
// to disconnected (because it'll never be set otherwise)
if dependencies[feature: .forceOffline] {
DispatchQueue.global(qos: .default).async { [dependencies] in
dependencies.mutate(cache: .libSessionNetwork) { $0.setNetworkStatus(status: .disconnected) }
}
}
}
deinit {
@ -713,13 +721,18 @@ public extension LibSession {
return Fail(error: NetworkError.suspended).eraseToAnyPublisher()
}
switch network {
case .some(let existingNetwork):
switch (network, dependencies[feature: .forceOffline]) {
case (_, true):
return Fail(error: NetworkError.serviceUnavailable)
.delay(for: .seconds(1), scheduler: DispatchQueue.main)
.eraseToAnyPublisher()
case (.some(let existingNetwork), _):
return Just(existingNetwork)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
case .none:
case (.none, _):
let useTestnet: Bool = (dependencies[feature: .serviceNetwork] == .testnet)
let isMainApp: Bool = dependencies[singleton: .appContext].isMainApp
var error: [CChar] = [CChar](repeating: 0, count: 256)

@ -76,7 +76,7 @@ public final class ProfilePictureView: UIView {
case none
case crown
case rightPlus
case letter(Character)
case letter(Character, Bool)
func iconVerticalInset(for size: Size) -> CGFloat {
switch (self, size) {
@ -438,9 +438,9 @@ public final class ProfilePictureView: UIView {
imageView.isHidden = false
label.isHidden = true
case .letter(let character):
label.themeTextColor = .backgroundPrimary
backgroundView.themeBackgroundColor = .textPrimary
case .letter(let character, let dangerMode):
label.themeTextColor = (dangerMode ? .textPrimary : .backgroundPrimary)
backgroundView.themeBackgroundColor = (dangerMode ? .danger : .textPrimary)
label.isHidden = false
label.text = "\(character)"
}

@ -8,7 +8,7 @@ import Combine
public class Dependencies {
static let userInfoKey: CodingUserInfoKey = CodingUserInfoKey(rawValue: "session.dependencies.codingOptions")!
private static var _isRTLRetriever: Atomic<(Bool, () -> Bool)> = Atomic((false, { false }))
private static var _lastCreatedInstance: Atomic<Dependencies?> = Atomic(nil)
private let featureChangeSubject: PassthroughSubject<(String, String?, Any?), Never> = PassthroughSubject()
private var storage: Atomic<DependencyStorage> = Atomic(DependencyStorage())
@ -21,8 +21,12 @@ public class Dependencies {
// MARK: - Global Values, Timing and Async Handling
public static var isRTL: Bool {
let (requiresMainThread, retriever): (Bool, () -> Bool) = _isRTLRetriever.wrappedValue
/// We should avoid using this value wherever possible because it's not properly injected (which means unit tests won't work correctly
/// for anything accessed via this value)
public static var unsafeNonInjected: Dependencies { _lastCreatedInstance.wrappedValue ?? Dependencies() }
public var isRTL: Bool {
let (requiresMainThread, retriever): (Bool, () -> Bool) = storage.wrappedValue.isRTLRetriever
/// Determining `isRTL` might require running on the main thread (it may need to accesses UIKit), if it requires the main thread but
/// we are on a different thread then just default to `false` to prevent the background thread from potentially lagging and/or crashing
@ -37,7 +41,9 @@ public class Dependencies {
// MARK: - Initialization
private init() {}
private init() {
Dependencies._lastCreatedInstance.mutate { $0 = self }
}
internal init(forTesting: Bool) {}
public static func createEmpty() -> Dependencies { return Dependencies() }
@ -104,26 +110,26 @@ public class Dependencies {
}
public func set<S>(singleton: SingletonConfig<S>, to instance: S) {
threadSafeChange(for: singleton.identifier) {
threadSafeChange(for: singleton.identifier, of: .singleton) {
setValue(instance, typedStorage: .singleton(instance), key: singleton.identifier)
}
}
public func set<M, I>(cache: CacheConfig<M, I>, to instance: M) {
threadSafeChange(for: cache.identifier) {
threadSafeChange(for: cache.identifier, of: .cache) {
let value: Atomic<MutableCacheType> = Atomic(cache.mutableInstance(instance))
setValue(value, typedStorage: .cache(value), key: cache.identifier)
}
}
public func remove<M, I>(cache: CacheConfig<M, I>) {
threadSafeChange(for: cache.identifier) {
removeValue(cache.identifier)
threadSafeChange(for: cache.identifier, of: .cache) {
removeValue(cache.identifier, of: .cache)
}
}
public static func setIsRTLRetriever(requiresMainThread: Bool, isRTLRetriever: @escaping () -> Bool) {
_isRTLRetriever.mutate { $0 = (requiresMainThread, isRTLRetriever) }
public func setIsRTLRetriever(requiresMainThread: Bool, isRTLRetriever: @escaping () -> Bool) {
storage.mutate { $0.isRTLRetriever = (requiresMainThread, isRTLRetriever) }
}
}
@ -172,10 +178,10 @@ public extension Dependencies {
}
func set<T: FeatureOption>(feature: FeatureConfig<T>, to updatedFeature: T?) {
threadSafeChange(for: feature.identifier) {
threadSafeChange(for: feature.identifier, of: .feature) {
/// Update the cached & in-memory values
let instance: Feature<T> = (
getValue(feature.identifier) ??
getValue(feature.identifier, of: .feature) ??
feature.createInstance(self)
)
instance.setValue(to: updatedFeature, using: self)
@ -187,11 +193,11 @@ public extension Dependencies {
}
func reset<T: FeatureOption>(feature: FeatureConfig<T>) {
threadSafeChange(for: feature.identifier) {
threadSafeChange(for: feature.identifier, of: .feature) {
/// Reset the cached and in-memory values
let instance: Feature<T>? = getValue(feature.identifier)
let instance: Feature<T>? = getValue(feature.identifier, of: .feature)
instance?.setValue(to: nil, using: self)
removeValue(feature.identifier)
removeValue(feature.identifier, of: .feature)
}
/// Notify observers
@ -272,8 +278,31 @@ public enum DependenciesError: Error {
private extension Dependencies {
struct DependencyStorage {
var initializationLocks: [String: NSLock] = [:]
var instances: [String: Value] = [:]
var initializationLocks: [Key: NSLock] = [:]
var instances: [Key: Value] = [:]
var isRTLRetriever: (Bool, () -> Bool) = (false, { false })
struct Key: Hashable, CustomStringConvertible {
enum Variant: String {
case singleton
case cache
case userDefaults
case feature
func key(_ identifier: String) -> Key {
return Key(identifier, of: self)
}
}
let identifier: String
let variant: Variant
var description: String { "\(variant): \(identifier)" }
init(_ identifier: String, of variant: Variant) {
self.identifier = identifier
self.variant = variant
}
}
enum Value {
case singleton(Any)
@ -281,6 +310,15 @@ private extension Dependencies {
case userDefaults(UserDefaultsType)
case feature(any FeatureType)
func distinctKey(for identifier: String) -> Key {
switch self {
case .singleton: return Key(identifier, of: .singleton)
case .cache: return Key(identifier, of: .cache)
case .userDefaults: return Key(identifier, of: .userDefaults)
case .feature: return Key(identifier, of: .feature)
}
}
func value<T>(as type: T.Type) -> T? {
switch self {
case .singleton(let value): return value as? T
@ -329,14 +367,14 @@ private extension Dependencies {
constructor: DependencyStorage.Constructor<Value>
) -> Value {
/// If we already have an instance then just return that
if let existingValue: Value = getValue(identifier) {
if let existingValue: Value = getValue(identifier, of: constructor.variant) {
return existingValue
}
return threadSafeChange(for: identifier) {
return threadSafeChange(for: identifier, of: constructor.variant) {
/// Now that we are within a synchronized group, check to make sure an instance wasn't created while we were waiting to
/// enter the group
if let existingValue: Value = getValue(identifier) {
if let existingValue: Value = getValue(identifier, of: constructor.variant) {
return existingValue
}
@ -347,11 +385,11 @@ private extension Dependencies {
}
/// Convenience method to retrieve the existing dependency instance from memory in a thread-safe way
private func getValue<T>(_ key: String) -> T? {
guard let typedValue: DependencyStorage.Value = storage.wrappedValue.instances[key] else { return nil }
private func getValue<T>(_ key: String, of variant: DependencyStorage.Key.Variant) -> T? {
guard let typedValue: DependencyStorage.Value = storage.wrappedValue.instances[variant.key(key)] else { return nil }
guard let result: T = typedValue.value(as: T.self) else {
/// If there is a value stored for the key, but it's not the right type then something has gone wrong, and we should log
Log.critical("Failed to convert stored dependency '\(key)' to expected type: \(T.self)")
Log.critical("Failed to convert stored dependency '\(variant.key(key))' to expected type: \(T.self)")
return nil
}
@ -360,13 +398,13 @@ private extension Dependencies {
/// Convenience method to store a dependency instance in memory in a thread-safe way
@discardableResult private func setValue<T>(_ value: T, typedStorage: DependencyStorage.Value, key: String) -> T {
storage.mutate { $0.instances[key] = typedStorage }
storage.mutate { $0.instances[typedStorage.distinctKey(for: key)] = typedStorage }
return value
}
/// Convenience method to remove a dependency instance from memory in a thread-safe way
private func removeValue(_ key: String) {
storage.mutate { $0.instances.removeValue(forKey: key) }
private func removeValue(_ key: String, of variant: DependencyStorage.Key.Variant) {
storage.mutate { $0.instances.removeValue(forKey: variant.key(key)) }
}
/// This function creates an `NSLock` for the given identifier which allows us to block instance creation on a per-identifier basis
@ -374,14 +412,14 @@ private extension Dependencies {
///
/// **Note:** This `NSLock` is an additional mechanism on top of the `Atomic<T>` because the interface is a little simpler
/// and we don't need to wrap every instance within `Atomic<T>` this way
@discardableResult private func threadSafeChange<T>(for identifier: String, change: () -> T) -> T {
@discardableResult private func threadSafeChange<T>(for identifier: String, of variant: DependencyStorage.Key.Variant, change: () -> T) -> T {
let lock: NSLock = storage.mutate { storage in
if let existing = storage.initializationLocks[identifier] {
if let existing = storage.initializationLocks[variant.key(identifier)] {
return existing
}
let lock: NSLock = NSLock()
storage.initializationLocks[identifier] = lock
storage.initializationLocks[variant.key(identifier)] = lock
return lock
}
lock.lock()
@ -395,10 +433,11 @@ private extension Dependencies {
private extension Dependencies.DependencyStorage {
struct Constructor<T> {
let variant: Key.Variant
let create: () -> (typedStorage: Dependencies.DependencyStorage.Value, value: T)
static func singleton(_ constructor: @escaping () -> T) -> Constructor<T> {
return Constructor {
return Constructor(variant: .singleton) {
let instance: T = constructor()
return (.singleton(instance), instance)
@ -406,7 +445,7 @@ private extension Dependencies.DependencyStorage {
}
static func cache(_ constructor: @escaping () -> T) -> Constructor<T> where T: Atomic<MutableCacheType> {
return Constructor {
return Constructor(variant: .cache) {
let instance: T = constructor()
return (.cache(instance), instance)
@ -414,7 +453,7 @@ private extension Dependencies.DependencyStorage {
}
static func userDefaults(_ constructor: @escaping () -> T) -> Constructor<T> where T == UserDefaultsType {
return Constructor {
return Constructor(variant: .userDefaults) {
let instance: T = constructor()
return (.userDefaults(instance), instance)
@ -422,7 +461,7 @@ private extension Dependencies.DependencyStorage {
}
static func feature(_ constructor: @escaping () -> T) -> Constructor<T> where T: FeatureType {
return Constructor {
return Constructor(variant: .feature) {
let instance: T = constructor()
return (.feature(instance), instance)

@ -9,6 +9,14 @@ public final class Features {
}
public extension FeatureStorage {
static let showStringKeys: FeatureConfig<Bool> = Dependencies.create(
identifier: "showStringKeys"
)
static let forceOffline: FeatureConfig<Bool> = Dependencies.create(
identifier: "forceOffline"
)
static let debugDisappearingMessageDurations: FeatureConfig<Bool> = Dependencies.create(
identifier: "debugDisappearingMessageDurations"
)
@ -57,6 +65,14 @@ public extension FeatureStorage {
static let updatedGroupsAllowInviteById: FeatureConfig<Bool> = Dependencies.create(
identifier: "updatedGroupsAllowInviteById"
)
static let updatedGroupsDeleteBeforeNow: FeatureConfig<Bool> = Dependencies.create(
identifier: "updatedGroupsDeleteBeforeNow"
)
static let updatedGroupsDeleteAttachmentsBeforeNow: FeatureConfig<Bool> = Dependencies.create(
identifier: "updatedGroupsDeleteAttachmentsBeforeNow"
)
}
// MARK: - FeatureOption

@ -30,6 +30,10 @@ final public class LocalizationHelper: CustomStringConvertible {
}
public func localized() -> String {
guard !Dependencies.unsafeNonInjected[feature: .showStringKeys] else {
return "[\(template)]"
}
// Use English as the default string if the translation is empty
let defaultString: String = {
if let englishPath = Bundle.main.path(forResource: "en", ofType: "lproj"), let englishBundle = Bundle(path: englishPath) {

@ -8,9 +8,9 @@ extension UIEdgeInsets {
public init(top: CGFloat, leading: CGFloat, bottom: CGFloat, trailing: CGFloat) {
self.init(
top: top,
left: (Dependencies.isRTL ? trailing : leading),
left: (Dependencies.unsafeNonInjected.isRTL ? trailing : leading),
bottom: bottom,
right: (Dependencies.isRTL ? leading : trailing)
right: (Dependencies.unsafeNonInjected.isRTL ? leading : trailing)
)
}
}

@ -123,7 +123,7 @@ public extension UTType {
let mimeType: String = preferredMIMEType,
let fileExtension: String = UTType.genericExtensionTypesToMimeTypes
.first(where: { _, value in value == mimeType })?
.value
.key
else { return preferredFilenameExtension }
return fileExtension

@ -65,12 +65,12 @@ public extension UIViewController {
let backButton: UIButton = UIButton(type: .custom)
// Nudge closer to the left edge to match default back button item.
let extraLeftPadding: CGFloat = (Dependencies.isRTL ? 0 : -8)
let extraLeftPadding: CGFloat = (Dependencies.unsafeNonInjected.isRTL ? 0 : -8)
// Give some extra hit area to the back button. This is a little smaller
// than the default back button, but makes sense for our left aligned title
// view in the MessagesViewController
let extraRightPadding: CGFloat = (Dependencies.isRTL ? -0 : 10)
let extraRightPadding: CGFloat = (Dependencies.unsafeNonInjected.isRTL ? -0 : 10)
// Extra hit area above/below
let extraHeightPadding: CGFloat = 8

Loading…
Cancel
Save