tls sasl
This commit is contained in:
1 parent
e3fcee3351
commit
d4b4c92759
29 files changed
+2177
-61
No files matched your search
@@ -53,6 +53,16 @@ struct AudioMessagePlayer: View {
|
||||
.font(.caption2.monospacedDigit())
|
||||
.foregroundStyle(secondaryText)
|
||||
Spacer()
|
||||
Button(action: saveToFiles) {
|
||||
Image(systemName: "square.and.arrow.down")
|
||||
.font(.caption.weight(.bold))
|
||||
.foregroundStyle(accent)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 3)
|
||||
.background(accent.opacity(0.14), in: Capsule())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Сохранить в Файлы")
|
||||
Button(action: playback.cycleVoiceRate) {
|
||||
Text(rateText)
|
||||
.font(.caption2.monospacedDigit().weight(.bold))
|
||||
@@ -95,6 +105,13 @@ struct AudioMessagePlayer: View {
|
||||
countStyle: .file
|
||||
))
|
||||
}
|
||||
Spacer()
|
||||
Button(action: saveToFiles) {
|
||||
Image(systemName: "square.and.arrow.down")
|
||||
.foregroundStyle(accent)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Сохранить в Файлы")
|
||||
}
|
||||
.font(.caption2.monospacedDigit())
|
||||
.foregroundStyle(secondaryText)
|
||||
@@ -185,6 +202,10 @@ struct AudioMessagePlayer: View {
|
||||
message.direction == .outgoing ? .white.opacity(0.78) : .secondary
|
||||
}
|
||||
|
||||
private func saveToFiles() {
|
||||
Task { await MediaDownloadService.save(message, model: model) }
|
||||
}
|
||||
|
||||
private func togglePlayback() {
|
||||
guard let localURL else {
|
||||
Task {
|
||||
|
||||
@@ -45,6 +45,7 @@ struct MediaViewer: View {
|
||||
@State private var isTrackingDismissGesture = false
|
||||
@State private var isCompletingDismissal = false
|
||||
@State private var isPhotoZoomed = false
|
||||
@State private var saveErrorMessage: String?
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
@@ -65,6 +66,16 @@ struct MediaViewer: View {
|
||||
.lineLimit(1)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: save) {
|
||||
Image(systemName: "square.and.arrow.down")
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.foregroundStyle(.white)
|
||||
.frame(width: 36, height: 36)
|
||||
.background(.white.opacity(0.16), in: Circle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.accessibilityLabel("Сохранить")
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 12)
|
||||
@@ -96,6 +107,11 @@ struct MediaViewer: View {
|
||||
.accessibilityAction(.escape) {
|
||||
closeImmediately()
|
||||
}
|
||||
.alert("Ошибка сохранения", isPresented: saveErrorBinding) {
|
||||
Button("OK", role: .cancel) {}
|
||||
} message: {
|
||||
Text(saveErrorMessage ?? "Не удалось сохранить медиа.")
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
.onExitCommand {
|
||||
@@ -183,6 +199,27 @@ struct MediaViewer: View {
|
||||
onClose()
|
||||
}
|
||||
|
||||
private var saveErrorBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { saveErrorMessage != nil },
|
||||
set: { visible in if !visible { saveErrorMessage = nil } }
|
||||
)
|
||||
}
|
||||
|
||||
private func save() {
|
||||
Task {
|
||||
do {
|
||||
try await MediaDownloadService.save(
|
||||
url: item.url,
|
||||
kind: item.kind,
|
||||
filename: item.title
|
||||
)
|
||||
} catch {
|
||||
saveErrorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func dismissalProgress(in containerHeight: CGFloat) -> CGFloat {
|
||||
min(1, abs(dragOffset) / max(1, containerHeight * 0.62))
|
||||
}
|
||||
|
||||
@@ -81,6 +81,13 @@ struct MessageBubble: View {
|
||||
Label("Копировать текст", systemImage: "doc.on.doc")
|
||||
}
|
||||
}
|
||||
if message.kind.isMedia, !message.isRetracted {
|
||||
Button {
|
||||
Task { await MediaDownloadService.save(message, model: model) }
|
||||
} label: {
|
||||
Label("Сохранить", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
}
|
||||
if model.canRetryMediaMessage(message) {
|
||||
Button(action: onRetry) {
|
||||
Label("Повторить отправку", systemImage: "arrow.clockwise")
|
||||
|
||||
@@ -98,6 +98,15 @@ private struct DeviceRow: View {
|
||||
systemImage: device.isActive ? "iphone" : "iphone.slash"
|
||||
)
|
||||
.font(.headline)
|
||||
Text(device.protocolName)
|
||||
.font(.caption2.weight(.medium))
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.foregroundStyle(device.isOMEMO2 ? Color.indigo : Color.secondary)
|
||||
.background(
|
||||
(device.isOMEMO2 ? Color.indigo : Color.secondary).opacity(0.14),
|
||||
in: Capsule()
|
||||
)
|
||||
Spacer()
|
||||
Button(action: toggleVerification) {
|
||||
Label(trustTitle, systemImage: trustIcon)
|
||||
|
||||
@@ -4,6 +4,7 @@ import SwiftUI
|
||||
|
||||
struct RootView: View {
|
||||
@ObservedObject var model: AppModel
|
||||
@State private var toastDismissTask: Task<Void, Never>?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -32,17 +33,55 @@ struct RootView: View {
|
||||
.transition(.opacity.combined(with: .scale(scale: 1.015)))
|
||||
.zIndex(200)
|
||||
}
|
||||
|
||||
if let info = model.informationalMessage {
|
||||
VStack {
|
||||
toast(info)
|
||||
Spacer()
|
||||
}
|
||||
.transition(.move(edge: .top).combined(with: .opacity))
|
||||
.zIndex(300)
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.22), value: model.account?.id)
|
||||
.animation(.easeInOut(duration: 0.2), value: model.mediaViewerItem?.id)
|
||||
.animation(.easeInOut(duration: 0.2), value: model.activeCall?.id)
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.85), value: model.informationalMessage)
|
||||
.alert("Luma", isPresented: errorBinding) {
|
||||
Button("OK") { model.clearError() }
|
||||
} message: {
|
||||
Text(model.errorMessage ?? "Неизвестная ошибка")
|
||||
}
|
||||
.quickLookPreview($model.previewURL)
|
||||
.onChange(of: model.informationalMessage) { _, newValue in
|
||||
guard newValue != nil else { return }
|
||||
toastDismissTask?.cancel()
|
||||
toastDismissTask = Task { @MainActor in
|
||||
try? await Task.sleep(nanoseconds: 2_500_000_000)
|
||||
guard !Task.isCancelled else { return }
|
||||
model.clearInformationalMessage()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact Telegram-style confirmation toast shown for a couple of
|
||||
/// seconds above every layer (lists, media viewer, calls).
|
||||
private func toast(_ message: String) -> some View {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundStyle(Color.accentColor)
|
||||
Text(message)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.lineLimit(2)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 10)
|
||||
.background(.regularMaterial, in: Capsule())
|
||||
.overlay(Capsule().strokeBorder(Color.primary.opacity(0.06)))
|
||||
.shadow(color: .black.opacity(0.18), radius: 12, y: 4)
|
||||
.padding(.top, 8)
|
||||
.accessibilityIdentifier("info-toast")
|
||||
}
|
||||
|
||||
private var errorBinding: Binding<Bool> {
|
||||
|
||||
@@ -55,8 +55,14 @@ struct ServerInfoView: View {
|
||||
Section("Это программное обеспечение, работающее на вашем сервере.") {
|
||||
softwareEntry(info)
|
||||
}
|
||||
Section("Параметры подключения к вашему серверу.") {
|
||||
connectionRows()
|
||||
Section {
|
||||
connectionRows(info)
|
||||
} header: {
|
||||
Text("Параметры подключения к вашему серверу.")
|
||||
} footer: {
|
||||
Text(
|
||||
"Luma автоматически выбирает самый безопасный вариант: при TLS договаривается о максимальной версии (1.3 > 1.2), а при входе — о самом стойком SASL-механизме, который поддерживает сервер."
|
||||
)
|
||||
}
|
||||
Section("Современные возможности XMPP, обнаруженные на вашем сервере после входа.") {
|
||||
ForEach(info.capabilities) { capability in
|
||||
@@ -123,7 +129,7 @@ struct ServerInfoView: View {
|
||||
.listRowBackground(Color.gray.opacity(0.16))
|
||||
}
|
||||
|
||||
private func connectionRows() -> some View {
|
||||
private func connectionRows(_ info: ServerInformation) -> some View {
|
||||
Group {
|
||||
if let account = model.account {
|
||||
if let host = account.manualHost {
|
||||
@@ -138,6 +144,19 @@ struct ServerInfoView: View {
|
||||
}
|
||||
LabeledContent("Ресурс", value: account.effectiveResource)
|
||||
}
|
||||
if let tlsVersion = info.tlsVersion {
|
||||
LabeledContent(
|
||||
"Версия TLS",
|
||||
value: info.tlsCipher.map { "\(tlsVersion) · \($0)" } ?? tlsVersion
|
||||
)
|
||||
} else {
|
||||
LabeledContent("Версия TLS", value: "Неизвестно")
|
||||
}
|
||||
if let saslMechanism = info.saslMechanism {
|
||||
LabeledContent("SASL-механизм", value: saslMechanism)
|
||||
} else {
|
||||
LabeledContent("SASL-механизм", value: "Неизвестно")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user