Баннер авторизации нового устройства в чат-листе (Telegram parity) + фикс навигации Backup
This commit is contained in:
@@ -718,7 +718,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Rosetta/Rosetta.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Rosetta/Rosetta.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 38;
|
CURRENT_PROJECT_VERSION = 39;
|
||||||
DEVELOPMENT_TEAM = QN8Z263QGX;
|
DEVELOPMENT_TEAM = QN8Z263QGX;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -734,7 +734,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.7;
|
MARKETING_VERSION = 1.3.8;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev;
|
PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -758,7 +758,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Rosetta/Rosetta.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Rosetta/Rosetta.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 38;
|
CURRENT_PROJECT_VERSION = 39;
|
||||||
DEVELOPMENT_TEAM = QN8Z263QGX;
|
DEVELOPMENT_TEAM = QN8Z263QGX;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
@@ -774,7 +774,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.3.7;
|
MARKETING_VERSION = 1.3.8;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev;
|
PRODUCT_BUNDLE_IDENTIFIER = com.rosetta.dev;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@@ -949,7 +949,7 @@
|
|||||||
C19929D9466573F31997B2C0 /* Release */,
|
C19929D9466573F31997B2C0 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
853F295D2F4B50410092AD05 /* Build configuration list for PBXProject "Rosetta" */ = {
|
853F295D2F4B50410092AD05 /* Build configuration list for PBXProject "Rosetta" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
@@ -958,7 +958,7 @@
|
|||||||
853F296C2F4B50420092AD05 /* Release */,
|
853F296C2F4B50420092AD05 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
853F296D2F4B50420092AD05 /* Build configuration list for PBXNativeTarget "Rosetta" */ = {
|
853F296D2F4B50420092AD05 /* Build configuration list for PBXNativeTarget "Rosetta" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
@@ -967,7 +967,7 @@
|
|||||||
853F296F2F4B50420092AD05 /* Release */,
|
853F296F2F4B50420092AD05 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
A8D200712F9000010092AD05 /* Build configuration list for PBXNativeTarget "RosettaUITests" */ = {
|
A8D200712F9000010092AD05 /* Build configuration list for PBXNativeTarget "RosettaUITests" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
@@ -976,7 +976,7 @@
|
|||||||
A8D200622F9000010092AD05 /* Release */,
|
A8D200622F9000010092AD05 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
B5D2E60ADEB8AE2E8F7615C6 /* Build configuration list for PBXNativeTarget "RosettaNotificationService" */ = {
|
B5D2E60ADEB8AE2E8F7615C6 /* Build configuration list for PBXNativeTarget "RosettaNotificationService" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
@@ -985,7 +985,7 @@
|
|||||||
0140D6320A9CF4B5E933E0B1 /* Debug */,
|
0140D6320A9CF4B5E933E0B1 /* Debug */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
LA00000062F8D22220092AD05 /* Build configuration list for PBXNativeTarget "RosettaLiveActivityWidget" */ = {
|
LA00000062F8D22220092AD05 /* Build configuration list for PBXNativeTarget "RosettaLiveActivityWidget" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
@@ -994,7 +994,7 @@
|
|||||||
LA00000082F8D22220092AD05 /* Release */,
|
LA00000082F8D22220092AD05 /* Release */,
|
||||||
);
|
);
|
||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Release"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
|
|||||||
@@ -12,23 +12,20 @@ enum ReleaseNotes {
|
|||||||
version: appVersion,
|
version: appVersion,
|
||||||
body: """
|
body: """
|
||||||
|
|
||||||
**Выделение и пересылка сообщений**
|
**Пересылка сообщений**
|
||||||
Долгое нажатие на сообщение для выделения нескольких. Массовая пересылка, удаление и отправка — все типы: текст, фото, голосовые, файлы, приглашения в группы.
|
Preview bar в composer при пересылке. Навигация в целевой чат. Поддержка всех типов: текст, фото, голосовые, файлы, инвайты.
|
||||||
|
|
||||||
**Голосовые сообщения**
|
**Прогресс загрузки фото**
|
||||||
Запись, предпросмотр, lock-to-record, кросс-платформенная совместимость Desktop/Android (WebM/Opus ↔ M4A). Waveform, воспроизведение, blob-анимация.
|
Telegram-style progress ring при отправке фото и файлов. Реальный прогресс CDN upload до 100%.
|
||||||
|
|
||||||
**Создание групп**
|
**Групповые аватарки**
|
||||||
Новый флоу: поиск контактов, мульти-выбор, glass UI, фото и описание группы. Можно создать группу без участников.
|
Отправка и шифрование аватарок в группах. Desktop parity.
|
||||||
|
|
||||||
**UIKit миграция**
|
**Индикация прочтения**
|
||||||
Chat Detail, Chat List header, Appearance, Settings — полный перевод на UIKit для Telegram-level производительности.
|
Telegram-exact галочки в чат-листе и баблах. Корректный unread badge после синхронизации.
|
||||||
|
|
||||||
**Tab Bar**
|
**Плавные анимации**
|
||||||
Редизайн 1:1 Telegram — dual-layer маскировка, Lottie-иконки, плавные анимации и badge.
|
Вставка сообщений с spring-анимацией. Skeleton без мерцания при открытии кешированных чатов. Date pill всегда видим при скролле.
|
||||||
|
|
||||||
**Пуш-уведомления**
|
|
||||||
Аватарки в системных пушах. In-app баннер Telegram parity. Групповые пуши, Desktop-suppression, 65+ тестов.
|
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -113,6 +113,10 @@ final class ChatListRootViewController: UIViewController, UINavigationController
|
|||||||
private var openChatObserver: NSObjectProtocol?
|
private var openChatObserver: NSObjectProtocol?
|
||||||
private var didBecomeActiveObserver: NSObjectProtocol?
|
private var didBecomeActiveObserver: NSObjectProtocol?
|
||||||
|
|
||||||
|
// Device alert banner (shown when new device tries to login)
|
||||||
|
private var deviceAlertBanner: DeviceAlertBannerView?
|
||||||
|
private var lastPendingDeviceId: String?
|
||||||
|
|
||||||
// PERF: Throttle render to prevent rapid-fire UI updates (blue flash bug).
|
// PERF: Throttle render to prevent rapid-fire UI updates (blue flash bug).
|
||||||
private var pendingRenderWork: DispatchWorkItem?
|
private var pendingRenderWork: DispatchWorkItem?
|
||||||
private var lastRenderTime: CFAbsoluteTime = 0
|
private var lastRenderTime: CFAbsoluteTime = 0
|
||||||
@@ -468,6 +472,7 @@ final class ChatListRootViewController: UIViewController, UINavigationController
|
|||||||
_ = DialogRepository.shared.dialogs
|
_ = DialogRepository.shared.dialogs
|
||||||
_ = SessionManager.shared.syncBatchInProgress
|
_ = SessionManager.shared.syncBatchInProgress
|
||||||
_ = ProtocolManager.shared.connectionState
|
_ = ProtocolManager.shared.connectionState
|
||||||
|
_ = ProtocolManager.shared.pendingDeviceVerification
|
||||||
_ = AvatarRepository.shared.avatarVersion
|
_ = AvatarRepository.shared.avatarVersion
|
||||||
} onChange: { [weak self] in
|
} onChange: { [weak self] in
|
||||||
Task { @MainActor [weak self] in
|
Task { @MainActor [weak self] in
|
||||||
@@ -503,9 +508,88 @@ final class ChatListRootViewController: UIViewController, UINavigationController
|
|||||||
|
|
||||||
private func render() {
|
private func render() {
|
||||||
updateNavigationTitle()
|
updateNavigationTitle()
|
||||||
|
updateDeviceAlertBanner()
|
||||||
renderList()
|
renderList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func updateDeviceAlertBanner() {
|
||||||
|
let pending = ProtocolManager.shared.pendingDeviceVerification
|
||||||
|
let newId = pending?.deviceId
|
||||||
|
|
||||||
|
// No change — skip
|
||||||
|
guard newId != lastPendingDeviceId else { return }
|
||||||
|
lastPendingDeviceId = newId
|
||||||
|
|
||||||
|
if let device = pending {
|
||||||
|
showDeviceAlertBanner(device: device)
|
||||||
|
} else {
|
||||||
|
hideDeviceAlertBanner()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showDeviceAlertBanner(device: DeviceEntry) {
|
||||||
|
guard deviceAlertBanner == nil else {
|
||||||
|
deviceAlertBanner?.configure(deviceName: device.deviceName, deviceOs: device.deviceOs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let banner = DeviceAlertBannerView()
|
||||||
|
banner.configure(deviceName: device.deviceName, deviceOs: device.deviceOs)
|
||||||
|
banner.onAccept = { [weak self] in
|
||||||
|
ProtocolManager.shared.acceptDevice(device.deviceId)
|
||||||
|
self?.hideDeviceAlertBanner()
|
||||||
|
}
|
||||||
|
banner.onDecline = { [weak self] in
|
||||||
|
ProtocolManager.shared.declineDevice(device.deviceId)
|
||||||
|
self?.hideDeviceAlertBanner()
|
||||||
|
}
|
||||||
|
|
||||||
|
let inset: CGFloat = 10
|
||||||
|
let bannerY = headerTotalHeight + searchChromeHeight
|
||||||
|
banner.frame = CGRect(
|
||||||
|
x: inset,
|
||||||
|
y: bannerY,
|
||||||
|
width: view.bounds.width - inset * 2,
|
||||||
|
height: DeviceAlertBannerView.bannerHeight
|
||||||
|
)
|
||||||
|
banner.alpha = 0
|
||||||
|
banner.transform = CGAffineTransform(translationX: 0, y: -20)
|
||||||
|
view.insertSubview(banner, belowSubview: searchHeaderView)
|
||||||
|
deviceAlertBanner = banner
|
||||||
|
|
||||||
|
// Push list content down via the collection view inside listController
|
||||||
|
let extraInset = DeviceAlertBannerView.bannerHeight + inset
|
||||||
|
if let cv = listController.view.subviews.first(where: { $0 is UICollectionView }) as? UICollectionView {
|
||||||
|
cv.contentInset.top += extraInset
|
||||||
|
}
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.3, delay: 0, options: .curveEaseOut) {
|
||||||
|
banner.alpha = 1
|
||||||
|
banner.transform = .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func hideDeviceAlertBanner() {
|
||||||
|
guard let banner = deviceAlertBanner else { return }
|
||||||
|
deviceAlertBanner = nil
|
||||||
|
lastPendingDeviceId = nil
|
||||||
|
|
||||||
|
let inset: CGFloat = 10
|
||||||
|
let extraInset = DeviceAlertBannerView.bannerHeight + inset
|
||||||
|
|
||||||
|
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseIn, animations: {
|
||||||
|
banner.alpha = 0
|
||||||
|
banner.transform = CGAffineTransform(translationX: 0, y: -20)
|
||||||
|
}) { _ in
|
||||||
|
banner.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore list inset
|
||||||
|
if let cv = listController.view.subviews.first(where: { $0 is UICollectionView }) as? UICollectionView {
|
||||||
|
cv.contentInset.top = max(0, cv.contentInset.top - extraInset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Telegram-style toolbar fade: 0.14s linear out, 0.3s linear in
|
/// Telegram-style toolbar fade: 0.14s linear out, 0.3s linear in
|
||||||
private func animateToolbarForSearch(active: Bool) {
|
private func animateToolbarForSearch(active: Bool) {
|
||||||
let targetAlpha: CGFloat = active ? 0.0 : 1.0
|
let targetAlpha: CGFloat = active ? 0.0 : 1.0
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import UIKit
|
||||||
|
|
||||||
|
/// Telegram-style banner shown at top of chat list when a new device login is detected.
|
||||||
|
/// Accept/Decline buttons let the user authorize or reject the device.
|
||||||
|
final class DeviceAlertBannerView: UIView {
|
||||||
|
|
||||||
|
var onAccept: (() -> Void)?
|
||||||
|
var onDecline: (() -> Void)?
|
||||||
|
|
||||||
|
private let glass = TelegramGlassUIView()
|
||||||
|
private let shieldIcon = UIImageView()
|
||||||
|
private let titleLabel = UILabel()
|
||||||
|
private let subtitleLabel = UILabel()
|
||||||
|
private let acceptButton = UIButton(type: .system)
|
||||||
|
private let declineButton = UIButton(type: .system)
|
||||||
|
|
||||||
|
static let bannerHeight: CGFloat = 80
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
setupSubviews()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) { fatalError() }
|
||||||
|
|
||||||
|
private func setupSubviews() {
|
||||||
|
// Glass background
|
||||||
|
glass.fixedCornerRadius = 14
|
||||||
|
glass.clipsToBounds = true
|
||||||
|
addSubview(glass)
|
||||||
|
|
||||||
|
// Shield icon
|
||||||
|
let config = UIImage.SymbolConfiguration(pointSize: 24, weight: .medium)
|
||||||
|
shieldIcon.image = UIImage(systemName: "shield.lefthalf.filled", withConfiguration: config)
|
||||||
|
shieldIcon.tintColor = UIColor(red: 0.14, green: 0.54, blue: 0.9, alpha: 1) // primaryBlue
|
||||||
|
shieldIcon.contentMode = .center
|
||||||
|
addSubview(shieldIcon)
|
||||||
|
|
||||||
|
// Title
|
||||||
|
titleLabel.text = "New device login"
|
||||||
|
titleLabel.font = .systemFont(ofSize: 15, weight: .semibold)
|
||||||
|
titleLabel.textColor = UIColor { $0.userInterfaceStyle == .dark ? .white : .black }
|
||||||
|
addSubview(titleLabel)
|
||||||
|
|
||||||
|
// Subtitle (device name)
|
||||||
|
subtitleLabel.font = .systemFont(ofSize: 13, weight: .regular)
|
||||||
|
subtitleLabel.textColor = UIColor { $0.userInterfaceStyle == .dark
|
||||||
|
? UIColor.white.withAlphaComponent(0.6)
|
||||||
|
: UIColor.black.withAlphaComponent(0.5)
|
||||||
|
}
|
||||||
|
subtitleLabel.lineBreakMode = .byTruncatingTail
|
||||||
|
addSubview(subtitleLabel)
|
||||||
|
|
||||||
|
// Accept button
|
||||||
|
acceptButton.setTitle("Accept", for: .normal)
|
||||||
|
acceptButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .semibold)
|
||||||
|
acceptButton.setTitleColor(.white, for: .normal)
|
||||||
|
acceptButton.backgroundColor = UIColor(red: 0.14, green: 0.54, blue: 0.9, alpha: 1)
|
||||||
|
acceptButton.layer.cornerRadius = 14
|
||||||
|
acceptButton.addTarget(self, action: #selector(acceptTapped), for: .touchUpInside)
|
||||||
|
addSubview(acceptButton)
|
||||||
|
|
||||||
|
// Decline button
|
||||||
|
declineButton.setTitle("Decline", for: .normal)
|
||||||
|
declineButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .medium)
|
||||||
|
declineButton.setTitleColor(UIColor(red: 0.9, green: 0.25, blue: 0.25, alpha: 1), for: .normal)
|
||||||
|
declineButton.addTarget(self, action: #selector(declineTapped), for: .touchUpInside)
|
||||||
|
addSubview(declineButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
func configure(deviceName: String, deviceOs: String) {
|
||||||
|
subtitleLabel.text = "\(deviceName) · \(deviceOs)"
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
glass.frame = bounds
|
||||||
|
|
||||||
|
let leftPad: CGFloat = 14
|
||||||
|
let iconSize: CGFloat = 36
|
||||||
|
|
||||||
|
shieldIcon.frame = CGRect(
|
||||||
|
x: leftPad,
|
||||||
|
y: (bounds.height - iconSize) / 2,
|
||||||
|
width: iconSize,
|
||||||
|
height: iconSize
|
||||||
|
)
|
||||||
|
|
||||||
|
let textX = shieldIcon.frame.maxX + 10
|
||||||
|
let buttonW: CGFloat = 70
|
||||||
|
let declineW: CGFloat = 65
|
||||||
|
let rightPad: CGFloat = 12
|
||||||
|
let buttonsW = buttonW + 8 + declineW + rightPad
|
||||||
|
let textW = bounds.width - textX - buttonsW
|
||||||
|
|
||||||
|
titleLabel.frame = CGRect(x: textX, y: 18, width: textW, height: 20)
|
||||||
|
subtitleLabel.frame = CGRect(x: textX, y: 40, width: textW, height: 18)
|
||||||
|
|
||||||
|
let btnY: CGFloat = (bounds.height - 28) / 2
|
||||||
|
acceptButton.frame = CGRect(
|
||||||
|
x: bounds.width - rightPad - declineW - 8 - buttonW,
|
||||||
|
y: btnY, width: buttonW, height: 28
|
||||||
|
)
|
||||||
|
declineButton.frame = CGRect(
|
||||||
|
x: bounds.width - rightPad - declineW,
|
||||||
|
y: btnY, width: declineW, height: 28
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func acceptTapped() { onAccept?() }
|
||||||
|
@objc private func declineTapped() { onDecline?() }
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ struct SafetyView: View {
|
|||||||
@State private var copiedPublicKey = false
|
@State private var copiedPublicKey = false
|
||||||
@State private var copiedPrivateKey = false
|
@State private var copiedPrivateKey = false
|
||||||
@State private var showDeleteConfirmation = false
|
@State private var showDeleteConfirmation = false
|
||||||
|
@State private var navController: UINavigationController?
|
||||||
|
|
||||||
private var publicKey: String {
|
private var publicKey: String {
|
||||||
SessionManager.shared.currentPublicKey
|
SessionManager.shared.currentPublicKey
|
||||||
@@ -38,6 +39,11 @@ struct SafetyView: View {
|
|||||||
.padding(.leading, 8)
|
.padding(.leading, 8)
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
}
|
}
|
||||||
|
.background(
|
||||||
|
NavigationControllerAccessor { nav in
|
||||||
|
self.navController = nav
|
||||||
|
}
|
||||||
|
)
|
||||||
.background(RosettaColors.Adaptive.background)
|
.background(RosettaColors.Adaptive.background)
|
||||||
.scrollContentBackground(.hidden)
|
.scrollContentBackground(.hidden)
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
@@ -129,7 +135,11 @@ struct SafetyView: View {
|
|||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
NavigationLink(value: SettingsDestination.backup) {
|
Button {
|
||||||
|
let hosting = UIHostingController(rootView: BackupView())
|
||||||
|
hosting.view.backgroundColor = UIColor(RosettaColors.Adaptive.background)
|
||||||
|
navController?.pushViewController(hosting, animated: true)
|
||||||
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Backup")
|
Text("Backup")
|
||||||
.font(.system(size: 15))
|
.font(.system(size: 15))
|
||||||
|
|||||||
Reference in New Issue
Block a user