some fixes
Build Unsigned iOS ipa / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned macOS App / Build macOS ZIP (push) Canceled after 0s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s

This commit is contained in:
wt committed 2026-09-27 21:43:08 +07:00
1 parent e1f27cd8b2
commit 95a1c62b54
16 files changed
+137 -51

No files matched your search

+30 -12
View File
@@ -2485,26 +2485,44 @@ final class AppModel: ObservableObject {
stashPendingReadMarker(envelope, cameFromPeer: cameFromPeer)
}
/// Promotes a matching outgoing message to `.read` and, when the receipt
/// came from the peer (not from another own device), broadcasts the state
/// to the user's other devices via Carbons/MAM. Failed messages are never
/// Promotes the matching outgoing message and every earlier outgoing
/// message in the same conversation to `.read`. XEP-0333 markers are
/// cumulative in practice: a marker for message N means the peer has
/// displayed the whole conversation through N. Failed messages are never
/// promoted by a receipt.
private func markRead(at index: Int, cameFromPeer: Bool) {
let message = messages[index]
guard message.direction == .outgoing,
message.delivery != .read,
message.delivery != .failed,
!message.isRetracted
guard messages.indices.contains(index) else { return }
let target = messages[index]
guard target.direction == .outgoing,
target.delivery != .failed,
!target.isRetracted
else { return }
messages[index].delivery = .read
let targetConversation = target.conversationID
var changed = false
for candidateIndex in messages.indices {
let candidate = messages[candidateIndex]
guard candidate.conversationID == targetConversation,
candidate.direction == .outgoing,
candidate.delivery != .read,
candidate.delivery != .failed,
!candidate.isRetracted,
candidate.timestamp < target.timestamp
|| (candidate.timestamp == target.timestamp
&& candidate.clientID <= target.clientID)
else { continue }
messages[candidateIndex].delivery = .read
changed = true
}
guard changed else { return }
schedulePersist()
guard cameFromPeer else { return }
xmpp.syncReadState(
ReadStateSync.Envelope(
id: UUID().uuidString,
conversationJID: message.conversationID,
messageID: message.originID ?? message.clientID,
stanzaID: message.stanzaID,
conversationJID: target.conversationID,
messageID: target.originID ?? target.clientID,
stanzaID: target.stanzaID,
timestamp: Date()
))
}
@@ -31,11 +31,12 @@ enum VideoNoteRotationPolicy {
/// sensor (UIDevice.current.orientation) can report landscape or
/// face-up while the app stays portrait, which is exactly how a
/// portrait circle ended up encoded sideways.
@MainActor
static var currentInterfaceOrientation: InterfaceOrientation {
let orientation = UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first { $0.activationState == .foregroundActive }?
.interfaceOrientation
.effectiveGeometry.interfaceOrientation
switch orientation {
case .portraitUpsideDown: return .portraitUpsideDown
case .landscapeLeft: return .landscapeLeft
@@ -50,9 +50,9 @@ struct WatchVoiceMessage: Sendable {
}
#if os(iOS) && canImport(WatchConnectivity)
import WatchConnectivity
@preconcurrency import WatchConnectivity
final class PhoneWatchBridge: NSObject, WCSessionDelegate {
final class PhoneWatchBridge: NSObject, WCSessionDelegate, @unchecked Sendable {
var onReply: ((String, String) -> Void)?
var onVoiceMessage: ((WatchVoiceMessage) -> Void)? {
didSet { deliverPendingVoiceMessages() }
@@ -1033,7 +1033,7 @@ final class VideoNoteRecorder: NSObject, ObservableObject, AVCaptureFileOutputRe
// consistent no matter which camera is recording.
let microphone =
AVCaptureDevice.default(
.builtInMicrophone,
.microphone,
for: .audio,
position: .back
)
@@ -1194,7 +1194,7 @@ private actor VideoNoteFileInspector {
]
)
videoInput.expectsMediaDataInRealTime = false
videoInput.transform = videoTrack.preferredTransform
videoInput.transform = (try? await videoTrack.load(.preferredTransform)) ?? .identity
let adaptor = AVAssetWriterInputPixelBufferAdaptor(
assetWriterInput: videoInput,
sourcePixelBufferAttributes: [
@@ -1397,7 +1397,7 @@ private actor VideoNoteFileInspector {
return nil
}
if !hasVideo {
transform = source.preferredTransform
transform = (try? await source.load(.preferredTransform)) ?? .identity
hasVideo = true
}
cursor = cursor + duration
@@ -1431,10 +1431,6 @@ private actor VideoNoteFileInspector {
try? FileManager.default.removeItem(at: outputURL)
return nil
}
guard exporter.status == .completed else {
try? FileManager.default.removeItem(at: outputURL)
return nil
}
return Finalization(url: outputURL, duration: composition.duration)
}
+45 -4
View File
@@ -27,6 +27,7 @@ private enum EmojiPickerPresentation: Identifiable {
struct ChatView: View {
private static let bottomAnchorID = "luma-chat-timeline-bottom"
private static let timelineCoordinateSpace = "luma-chat-timeline-space"
private static let localHistoryPageSize = 60
@ObservedObject var model: AppModel
let conversation: Conversation
@@ -75,6 +76,8 @@ struct ChatView: View {
@State private var captureSuspendsArchiveSync = false
@State private var pickerResetToken = UUID()
@State private var timelineEntries: [ChatTimelineEntry] = []
@State private var visibleLocalMessageCount = 0
@State private var localHistoryExhausted = false
/// Messages-style entrance state: which rows currently animate, which IDs
/// the timeline already showed, and whether the opening cascade of this
/// conversation has played.
@@ -150,6 +153,8 @@ struct ChatView: View {
_captureSuspendsArchiveSync = State(initialValue: false)
_pickerResetToken = State(initialValue: UUID())
_timelineEntries = State<[ChatTimelineEntry]>(initialValue: [])
_visibleLocalMessageCount = State(initialValue: 0)
_localHistoryExhausted = State(initialValue: false)
_entranceModes = State<[String: MessageBubbleEntrance.Mode]>(
initialValue: [:]
)
@@ -389,6 +394,8 @@ struct ChatView: View {
historyTopTriggerVisible = false
historyAutoContinueCount = 0
emptyHistoryRetryCount = 0
visibleLocalMessageCount = 0
localHistoryExhausted = false
entranceModes = [:]
knownEntranceIDs = []
hasPlayedOpeningEntrance = false
@@ -495,8 +502,21 @@ struct ChatView: View {
private func rebuildTimelineEntries(from newMessages: [ChatMessage]? = nil) {
let source = newMessages ?? messages
timelineEntries = ChatTimelineEntry.make(from: source)
updateEntranceModes(for: source)
if visibleLocalMessageCount == 0 {
visibleLocalMessageCount = min(Self.localHistoryPageSize, source.count)
} else if localHistoryExhausted {
visibleLocalMessageCount = source.count
} else {
visibleLocalMessageCount = min(visibleLocalMessageCount, source.count)
}
localHistoryExhausted = visibleLocalMessageCount >= source.count
let visible = Array(source.suffix(visibleLocalMessageCount))
timelineEntries = ChatTimelineEntry.make(from: visible)
updateEntranceModes(for: visible)
}
private var hasMoreLocalHistory: Bool {
visibleLocalMessageCount < messages.count
}
/// Decides which rows play a Messages-style entrance after a rebuild.
@@ -588,7 +608,7 @@ struct ChatView: View {
ScrollView {
LazyVStack(spacing: 5) {
if hasCompletedInitialScroll,
model.hasMoreOlderHistory,
(hasMoreLocalHistory || model.hasMoreOlderHistory),
!timelineEntries.isEmpty
{
Color.clear
@@ -625,7 +645,7 @@ struct ChatView: View {
.onAppear {
// Empty timeline has no scroll gesture, so load
// the first page directly when it appears.
guard model.hasMoreOlderHistory,
guard hasMoreLocalHistory || model.hasMoreOlderHistory,
!model.isLoadingOlderHistory
else { return }
model
@@ -815,6 +835,17 @@ struct ChatView: View {
selectedMessageIDs.formIntersection(
Set(timelineEntries.map(\.id))
)
guard historyLoadAnchorID != nil,
!model.isLoadingOlderHistory,
historyTopTriggerVisible,
let anchor = historyLoadAnchorID
else { return }
Task { @MainActor in
await Task.yield()
proxy.scrollTo(anchor, anchor: .top)
historyLoadAnchorID = nil
historyTopTriggerArmed = true
}
}
if hasCompletedInitialScroll,
@@ -1995,6 +2026,16 @@ struct ChatView: View {
historyTopTriggerArmed = false
historyLoadAnchorID = timelineEntries.first?.id
historyLoadEntryCount = timelineEntries.count
if hasMoreLocalHistory {
visibleLocalMessageCount = min(
messages.count,
visibleLocalMessageCount + Self.localHistoryPageSize
)
localHistoryExhausted = visibleLocalMessageCount >= messages.count
rebuildTimelineEntries(from: messages)
historyTopTriggerArmed = true
return
}
model.loadOlderHistoryForSelectedConversation()
}
@@ -56,7 +56,7 @@ struct LocationMessagePreview: View {
}
private func openInMaps() {
let item = MKMapItem(placemark: MKPlacemark(coordinate: coordinate))
let item = MKMapItem(location: CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude), address: nil)
item.name = "Геопозиция из Luma"
item.openInMaps()
}
+4 -3
View File
@@ -3,7 +3,7 @@ import Combine
import CommonCrypto
import CryptoKit
import os
import Martin
@preconcurrency import Martin
import MartinOMEMO
extension XmppModuleIdentifier {
@@ -22,7 +22,7 @@ extension XmppModuleIdentifier {
/// `<bundle>` with `<spk>/<spks>/<ik>/<prekeys><pk>`).
/// The Double Ratchet sessions are shared with the legacy OMEMO module
/// through the same SignalStorage.
final class LumaOMEMO2Module: AbstractPEPModule, XmppModule {
final class LumaOMEMO2Module: AbstractPEPModule, XmppModule, @unchecked Sendable {
public static let ID = "omemo2"
public static let IDENTIFIER = XmppModuleIdentifier<LumaOMEMO2Module>()
public static let XMLNS = "urn:xmpp:omemo:2"
@@ -98,7 +98,8 @@ final class LumaOMEMO2Module: AbstractPEPModule, XmppModule {
return
}
let known = listEl.mapChildren(transform: { $0.getAttribute("id").flatMap(Int32.init) })
self.devicesQueue.async { self.devices[jid] = known }
let jidString = jid.stringValue
self.devicesQueue.async { self.devices[BareJID(jidString)] = known }
continuation.resume(returning: known)
}
}
@@ -28,7 +28,7 @@ final class LumaSaslFailureModule: XmppModuleBase, XmppModule {
}
func process(stanza: Stanza) throws {
let condition = stanza.findChild()?.name
let condition = stanza.firstChild(where: { _ in true })?.name
let text = stanza.firstChild(name: "text")?.value
lock.lock()
stored = Failure(condition: condition, text: text)
@@ -61,7 +61,7 @@ enum SCRAMHash {
func hash(data: Data) -> Data {
var output = [UInt8](repeating: 0, count: digestLength)
data.withUnsafeBytes { bytes in
_ = data.withUnsafeBytes { bytes in
switch self {
case .sha1:
CC_SHA1(bytes.baseAddress, CC_LONG(bytes.count), &output)
@@ -201,7 +201,7 @@ final class LumaTLSNetworkProcessor: SocketConnector.NetworkProcessor, SSLNetwor
override func read(data: Data) {
guard !handshakeFailed else { return }
ensureTLS()
guard let ssl, let readBIO else { return }
guard ssl != nil, let readBIO else { return }
data.withUnsafeBytes { raw in
_ = BIO_write(readBIO, raw.baseAddress, Int32(raw.count))
}
@@ -415,9 +415,7 @@ final class LumaTLSNetworkProcessor: SocketConnector.NetworkProcessor, SSLNetwor
switch certificateValidation {
case .default:
SecTrustSetPolicies(trust, SecPolicyCreateSSL(true, domain as CFString))
var result = SecTrustResultType.invalid
SecTrustEvaluate(trust, &result)
validated = result == .proceed || result == .unspecified
validated = SecTrustEvaluateWithError(trust, nil)
case .fingerprint(let fingerprint):
validated = Self.matchesFingerprint(leafDER, fingerprint: fingerprint)
case .customValidator(let validator):