Files
Luma/Scripts/verify.sh
T
2026-07-30 20:07:51 +07:00

641 lines
25 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
required=(
project.yml
Sources/App/LumaApp.swift
Sources/Shared/XMPP/XMPPService.swift
Sources/Shared/XMPP/LumaCallEngine.swift
Sources/Shared/XMPP/LumaRoomStore.swift
Sources/Shared/XMPP/LumaOMEMOStore.swift
Sources/Shared/Models/MediaMetadata.swift
Sources/Shared/Models/AttachmentDraft.swift
Sources/Shared/Models/MediaSendActivityTracker.swift
Sources/Shared/Models/ArchiveSyncCheckpoint.swift
Sources/Shared/Models/ArchiveSyncPagination.swift
Sources/Shared/Models/ArchiveSyncRecoveryPolicy.swift
Sources/Shared/Models/ArchiveSyncWorkBudget.swift
Sources/Shared/Models/ArchiveMessageBatchPolicy.swift
Sources/Shared/Models/ChatScrollPositionPolicy.swift
Sources/Shared/Models/ChatTimelineEntry.swift
Sources/Shared/Models/ChatTypingPolicy.swift
Sources/Shared/Models/MessageReaction.swift
Sources/Shared/Models/MediaPickerSelectionPolicy.swift
Sources/Shared/Models/VideoNoteRecordingCompletionPolicy.swift
Sources/Shared/Models/VideoNoteRecordingLifecycle.swift
Sources/Shared/Models/MessageReplySwipePolicy.swift
Sources/Shared/Models/MessageReplyFallback.swift
Sources/Shared/Models/CallSnapshot.swift
Sources/Shared/Models/NotificationPolicy.swift
Sources/Shared/Services/AudioMessageRecorder.swift
Sources/Shared/Services/MediaPlaybackCoordinator.swift
Sources/Shared/Services/MediaPreviewProcessor.swift
Sources/Shared/Services/MediaFileIO.swift
Sources/Shared/Services/ChatMediaImageCache.swift
Sources/Shared/Services/LocationProvider.swift
Sources/Shared/Services/VideoNoteRecorder.swift
Sources/Shared/UI/SystemPhotoCameraView.swift
Sources/Shared/UI/VideoNoteCaptureView.swift
Sources/Shared/UI/CallView.swift
Sources/Shared/UI/Components/AudioMessagePlayer.swift
Sources/Shared/UI/Components/BubbleShapes.swift
Sources/Shared/UI/Components/InlineVideoPlayer.swift
Sources/Shared/UI/Components/VideoAttachmentPreview.swift
Sources/Shared/UI/Components/VideoNotePreview.swift
Sources/Shared/UI/Components/PhotoAttachmentPreview.swift
Sources/Shared/UI/Components/ReplyThreadOverlay.swift
Sources/Shared/UI/Components/MediaViewer.swift
Sources/Shared/UI/Components/LocationMessagePreview.swift
Sources/Shared/UI/Components/RTCVideoRendererView.swift
Sources/Shared/UI/LocationPickerView.swift
Sources/Shared/UI/ForwardMessageView.swift
Sources/Shared/UI/NewGroupView.swift
Sources/Shared/UI/GroupInfoView.swift
Sources/Shared/UI/AttachmentPreviewView.swift
Sources/Watch/LumaWatchApp.swift
Sources/Watch/WatchSessionModel.swift
Sources/Watch/WatchViews.swift
Sources/Watch/WatchVoiceRecorder.swift
Tests/CallSDPOrderingTests.swift
Tests/CallSnapshotTests.swift
Tests/NotificationPolicyTests.swift
Tests/MediaSendActivityTrackerTests.swift
Tests/ComposerRecordingGestureTests.swift
Tests/MediaViewerDismissGestureTests.swift
Tests/ArchiveSyncCheckpointTests.swift
Tests/ArchiveSyncPaginationTests.swift
Tests/ArchiveSyncRecoveryPolicyTests.swift
Tests/ArchiveSyncWorkBudgetTests.swift
Tests/ArchiveMessageBatchPolicyTests.swift
Tests/ChatScrollPositionPolicyTests.swift
Tests/ChatTimelineEntryTests.swift
Tests/ChatTypingPolicyTests.swift
Tests/MessageReactionTests.swift
Tests/MediaFileIOTests.swift
Tests/MediaPickerSelectionPolicyTests.swift
Tests/VideoNoteRecordingCompletionPolicyTests.swift
Tests/VideoNoteRecordingLifecycleTests.swift
Tests/VideoNoteStopPolicyTests.swift
Tests/MessageReplySwipeTests.swift
Tests/WatchVoiceMessageTests.swift
Brand/LumaIcon-1024.png
Resources/AppAssets.xcassets/AppIcon.appiconset/Contents.json
Regenerate-Luma-Project.command
Scripts/regenerate-project.command
)
for file in "${required[@]}"; do
test -f "$file" || { echo "Missing $file"; exit 1; }
done
test "$(grep -c 'path: Resources/AppAssets.xcassets' project.yml)" -eq 3 || {
echo "Every app target must reference Resources/AppAssets.xcassets"
exit 1
}
test "$(grep -c 'ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon' project.yml)" -eq 3 || {
echo "Every app target must compile the AppIcon set"
exit 1
}
if grep -q 'type: application.watchapp2' project.yml; then
echo "The legacy WatchKit container cannot compile single-target SwiftUI watch sources"
exit 1
fi
grep -A8 '^ LumaWatch:' project.yml | grep -q 'type: application$' || {
echo "LumaWatch must be a modern single-target watchOS application"
exit 1
}
test "$(grep -c 'PRODUCT_NAME: LumaWatch' project.yml)" -eq 1 || {
echo "The watch product must use its own bundle filename"
exit 1
}
grep -q 'TARGETED_DEVICE_FAMILY: "4"' project.yml || {
echo "The watch target must select the watch device family"
exit 1
}
grep -q '<key>NSMicrophoneUsageDescription</key>' Config/LumaWatch-Info.plist || {
echo "The watch app microphone usage description is missing"
exit 1
}
grep -q 'session.transferFile(fileURL, metadata: metadata)' Sources/Watch/WatchSessionModel.swift || {
echo "Background Apple Watch voice transfer is missing"
exit 1
}
grep -q 'voiceTransferCacheKey' Sources/Watch/WatchSessionModel.swift || {
echo "Apple Watch transfer acknowledgements must survive app restarts"
exit 1
}
grep -q 'persistVoiceMessage(message)' Sources/Shared/Services/PhoneWatchBridge.swift || {
echo "Received Apple Watch voice files must survive iPhone app restarts"
exit 1
}
grep -q 'kind: \.voice' Sources/Shared/Models/AppModel.swift || {
echo "Apple Watch recordings are not connected to the voice media pipeline"
exit 1
}
test "$(grep -c -- '- package: WebRTC' project.yml)" -eq 2 || {
echo "WebRTC must be linked to the iOS/iPadOS and macOS targets"
exit 1
}
if grep -R -q 'ChatBubbleShape\|maximumMediaSize\|25 МБ' Sources/Shared; then
echo "Legacy bubble tails or the old client attachment limit are still present"
exit 1
fi
if grep -q 'DtlsSrtpKeyAgreement\|continualGatheringPolicy\|iceCandidatePoolSize = 5\|OfferToReceive' \
Sources/Shared/XMPP/LumaCallEngine.swift; then
echo "Legacy WebRTC peer-connection configuration is still present"
exit 1
fi
grep -q 'attempts.append(("public STUN"' Sources/Shared/XMPP/LumaCallEngine.swift || {
echo "WebRTC public STUN fallback is missing"
exit 1
}
grep -q 'attempts.append(("host candidates", \[\]))' Sources/Shared/XMPP/LumaCallEngine.swift || {
echo "WebRTC host-candidate fallback is missing"
exit 1
}
grep -q 'CallCandidateDispatchGate.canSend' Sources/Shared/XMPP/LumaCallEngine.swift || {
echo "Jingle ICE candidate signaling gate is missing"
exit 1
}
grep -q 'session.pinPeerResource(jid)' Sources/Shared/XMPP/LumaCallEngine.swift || {
echo "Jingle full-resource pinning is missing"
exit 1
}
grep -q 'return (jid: jmiJID, usesMessageInitiation: true)' Sources/Shared/XMPP/LumaCallEngine.swift || {
echo "JMI must target an advertised full JID"
exit 1
}
grep -q 'CallSDPOrdering.answerIndices' Sources/Shared/XMPP/LumaCallEngine.swift || {
echo "Outgoing video answer ordering guard is missing"
exit 1
}
grep -q 'CallSDPNormalization.martinParseInput' Sources/Shared/XMPP/LumaCallEngine.swift || {
echo "WebRTC-to-Martin SDP terminator normalization is missing"
exit 1
}
grep -q 'CallSDPCompatibility.repairLocalFIDMSIDs' Sources/Shared/XMPP/LumaCallEngine.swift || {
echo "Outgoing video FID/MSID compatibility repair is missing"
exit 1
}
grep -q 'historyHandler' Sources/Shared/XMPP/LumaCallEngine.swift || {
echo "Call completion history bridge is missing"
exit 1
}
grep -q 'callHistoryContent' Sources/Shared/UI/Components/MessageBubble.swift || {
echo "Call history message card is missing"
exit 1
}
grep -q 'defaultScrollAnchor(.bottom)' Sources/Shared/UI/ChatView.swift || {
echo "Chat timeline must open at the latest message"
exit 1
}
grep -q 'ArchiveSyncPagination' Sources/Shared/XMPP/XMPPService.swift || {
echo "MAM synchronization must validate every RSM cursor"
exit 1
}
grep -q 'guard activeQueryID == queryID else' Sources/Shared/XMPP/XMPPService.swift || {
echo "MAM results must be restricted to the active query ID"
exit 1
}
grep -q 'allowedSources.contains(source.stringValue.lowercased())' Sources/Shared/XMPP/XMPPService.swift || {
echo "MAM results must be restricted to the account archive source"
exit 1
}
grep -q 'archiveStanzaInbox.append' Sources/Shared/XMPP/XMPPService.swift || {
echo "Raw MAM stanzas must enter the bounded parser-queue inbox"
exit 1
}
if sed -n '/archivedMessagesPublisher/,/store(in: &cancellables)/p' \
Sources/Shared/XMPP/XMPPService.swift | grep -q 'receive(on: DispatchQueue.main)'; then
echo "Every raw MAM stanza must not be dispatched onto the main queue"
exit 1
fi
grep -q 'RSM.Query(lastItems: archiveBootstrapMessageLimit)' Sources/Shared/XMPP/XMPPService.swift || {
echo "Fresh MAM bootstrap must request the latest bounded page"
exit 1
}
grep -q 'archiveWorkBudget.recordCompletedPage' Sources/Shared/XMPP/XMPPService.swift || {
echo "Incremental MAM passes must have a finite foreground work budget"
exit 1
}
if grep -q 'scheduleArchiveRestart\|restartDelayNanoseconds' Sources/Shared/XMPP/XMPPService.swift; then
echo "Failed MAM queries must not restart themselves indefinitely"
exit 1
fi
grep -q 'archiveRetrySuppressedUntilActivation = true' Sources/Shared/XMPP/XMPPService.swift || {
echo "MAM failures must remain paused until a later app activation"
exit 1
}
grep -q 'pauseArchiveSync(client: client)' Sources/Shared/XMPP/XMPPService.swift || {
echo "MAM must stop cleanly while the app or camera is inactive"
exit 1
}
grep -q 'isArchiveSyncing = false' Sources/Shared/Models/AppModel.swift || {
echo "A transport disconnect must clear a stale MAM indicator"
exit 1
}
grep -q 'case archiveBatch(\[ArchiveMutation\])' Sources/Shared/XMPP/XMPPService.swift || {
echo "A decoded MAM pass must be exposed as one atomic mutation batch"
exit 1
}
grep -q 'applyArchiveBatch(mutations)' Sources/Shared/Models/AppModel.swift || {
echo "MAM mutations must be applied atomically before publishing SwiftUI state"
exit 1
}
grep -q 'decodeSliceSize = 1' Sources/Shared/Models/ArchiveMessageBatchPolicy.swift || {
echo "MAM decoding must remain limited to one stanza per main-actor slice"
exit 1
}
grep -q 'lastSuccessfulMAMCursor' Sources/Shared/Persistence/ChatArchive.swift || {
echo "The durable archive snapshot must persist the XEP-0313 UID cursor"
exit 1
}
grep -q 'ArchiveSyncCursorPolicy.requestPosition' Sources/Shared/XMPP/XMPPService.swift || {
echo "MAM reconnects must prefer the durable archive UID over timestamps"
exit 1
}
grep -q 'shouldFallbackFromStoredCursor' Sources/Shared/XMPP/XMPPService.swift || {
echo "An expired archive UID must have one bounded timestamp fallback"
exit 1
}
grep -q 'await Task.yield()' Sources/Shared/XMPP/XMPPService.swift || {
echo "MAM decoding must yield to scrolling and camera callbacks"
exit 1
}
grep -q 'pageApplyTimeoutNanoseconds' Sources/Shared/XMPP/XMPPService.swift || {
echo "MAM page application must have a finite watchdog"
exit 1
}
grep -q 'setArchiveSyncIndicator(false)' Sources/Shared/XMPP/XMPPService.swift || {
echo "Every MAM terminal path must be able to clear the indicator"
exit 1
}
grep -q 'selectedMessagesCacheConversationID' Sources/Shared/Models/AppModel.swift || {
echo "Chat timeline sorting must not repeat on every recording timer tick"
exit 1
}
if sed -n '/private func upsertMessage(/,/private func updateMessage/p' \
Sources/Shared/Models/AppModel.swift | grep -q 'messages.firstIndex'; then
echo "MAM insertion must use the indexed message lookup"
exit 1
fi
grep -q 'incrementUnread: envelope.isArchived ? false : nil' Sources/Shared/Models/AppModel.swift || {
echo "Historical MAM replay must not inflate unread counters"
exit 1
}
grep -q 'persistenceQueue.async' Sources/Shared/XMPP/LumaOMEMOStore.swift || {
echo "OMEMO state persistence must not block MAM decryption on the main actor"
exit 1
}
grep -q 'persistWhenQuiet' Sources/Shared/XMPP/LumaOMEMOStore.swift || {
echo "OMEMO persistence must wait for a quiet coalescing window"
exit 1
}
grep -q 'mediaFileIO.load' Sources/Shared/Models/AppModel.swift || {
echo "Large media files must be loaded outside the main actor"
exit 1
}
grep -q 'stageAttachmentDraft' Sources/Shared/Models/AppModel.swift || {
echo "Picker media must be copied outside the main actor before preview"
exit 1
}
grep -q 'NSFileCoordinator' Sources/Shared/Services/MediaFileIO.swift || {
echo "Security-scoped and iCloud media reads must be coordinated"
exit 1
}
grep -q 'preferredItemEncoding: .current' Sources/Shared/UI/ChatView.swift || {
echo "The photo picker must avoid unnecessary representation transcoding"
exit 1
}
grep -q 'MediaPickerSelectionPolicy.preferredOrder' Sources/Shared/UI/ChatView.swift || {
echo "Ambiguous photo/video providers must retain a fallback representation"
exit 1
}
grep -q 'item.loadTransferable(type: Data.self)' Sources/Shared/UI/ChatView.swift || {
echo "PhotosPicker images need a data fallback when no file representation is available"
exit 1
}
grep -q 'CGImageSourceGetType' Sources/Shared/UI/ChatView.swift || {
echo "Fallback image data must retain its real media type and extension"
exit 1
}
grep -q 'setArchiveSyncSuspendedForMediaWork' Sources/Shared/Models/AppModel.swift || {
echo "MAM must pause while gallery media is staged or sent"
exit 1
}
grep -q 'withMediaPreparationActivity' Sources/Shared/UI/ChatView.swift || {
echo "MAM must pause before Photos/iCloud item-provider loading starts"
exit 1
}
grep -q 'thumbnailData: draft.thumbnailData' Sources/Shared/Models/AppModel.swift || {
echo "Prepared gallery thumbnails must not be regenerated during send"
exit 1
}
grep -q 'thumbnailImages: \[UUID: DraftThumbnailImage\]' Sources/Shared/UI/AttachmentPreviewView.swift || {
echo "Attachment thumbnails must be decoded once and cached by draft ID"
exit 1
}
grep -q '@State private var player: AVPlayer?' Sources/Shared/UI/Components/MediaViewer.swift || {
echo "The full-screen video player must survive SwiftUI body updates"
exit 1
}
if grep -q 'archiveLookback\|page < 10' Sources/Shared/XMPP/XMPPService.swift; then
echo "MAM synchronization must not silently truncate history"
exit 1
fi
grep -q 'performInitialScrollAfterLayout' Sources/Shared/UI/ChatView.swift || {
echo "Deferred initial chat scroll is missing"
exit 1
}
grep -q 'scrollDismissesKeyboard(.interactively)' Sources/Shared/UI/ChatView.swift || {
echo "Interactive keyboard dismissal is missing"
exit 1
}
grep -q 'selectedTimelineEntries' Sources/Shared/UI/ChatView.swift || {
echo "Chat rows and day boundaries must use the cached stable timeline"
exit 1
}
grep -q 'jumpToLatestButton' Sources/Shared/UI/ChatView.swift || {
echo "The chat must expose a jump-to-latest button away from the bottom"
exit 1
}
grep -q 'ChatScrollMetricsObserver' Sources/Shared/UI/ChatView.swift || {
echo "iOS bottom-button state must follow native UIScrollView metrics"
exit 1
}
grep -q 'scheduleMetricsReport' Sources/Shared/UI/ChatView.swift || {
echo "iOS scroll metrics must be coalesced before publishing SwiftUI state"
exit 1
}
if sed -n '/func attachToAncestorScrollViewIfNeeded/,/func resetReportedState/p' \
Sources/Shared/UI/ChatView.swift | grep -q 'observedScrollView == nil'; then
echo "The iOS scroll observer must be able to attach to a replacement UIScrollView"
exit 1
fi
grep -q 'ChatScrollPositionPolicy.isNearBottom' Sources/Shared/UI/ChatView.swift || {
echo "The jump-to-latest button must follow a bounded bottom threshold"
exit 1
}
grep -q 'safeAreaInset(edge: .bottom' Sources/Shared/UI/ChatView.swift || {
echo "The glass composer must float without an opaque full-width background"
exit 1
}
if sed -n '/private var messageInput:/,/private var composerActionControl:/p' \
Sources/Shared/UI/ChatView.swift | grep -q 'DragGesture'; then
echo "The composer must not compete with vertical timeline scrolling"
exit 1
fi
grep -q 'ComposerRecordingGesturePolicy.resolution' Sources/Shared/UI/ChatView.swift || {
echo "Telegram-style recording gestures are missing"
exit 1
}
grep -q 'MessageReplySwipePolicy.shouldReply' Sources/Shared/UI/Components/MessageBubble.swift || {
echo "Horizontal reply swipe is missing"
exit 1
}
grep -q 'ChatMediaImageCache.image' Sources/Shared/UI/Components/PhotoAttachmentPreview.swift || {
echo "Visible chat thumbnails must not be decoded on every SwiftUI pass"
exit 1
}
if sed -n '/private var composer:/,/private var messageInput:/p' Sources/Shared/UI/ChatView.swift \
| grep -q 'background(.ultraThinMaterial)'; then
echo "The message composer must not have a full-width grey material background"
exit 1
fi
grep -q 'Image(systemName: "paperclip")' Sources/Shared/UI/ChatView.swift || {
echo "Telegram-style attachment control is missing"
exit 1
}
grep -q 'presentVideoNoteRecorder()' Sources/Shared/UI/ChatView.swift || {
echo "The attachment menu must open the dedicated video-note recorder"
exit 1
}
grep -q 'Label("Видеосообщение"' Sources/Shared/UI/ChatView.swift || {
echo "The attachment menu video-note action is missing"
exit 1
}
grep -q 'try recorder.start()' Sources/Shared/UI/VideoNoteCaptureView.swift || {
echo "The dedicated video-note screen must start recording after camera preparation"
exit 1
}
if grep -q 'Режим видеосообщения\|composerCaptureMode.*video' Sources/Shared/UI/ChatView.swift; then
echo "Video-note recording must not reuse the inline voice-message gesture"
exit 1
fi
grep -q 'resetCaptureGraph()' Sources/Shared/Services/VideoNoteRecorder.swift || {
echo "Reusable video-note capture cleanup is missing"
exit 1
}
grep -q 'sessionQueue.async' Sources/Shared/Services/VideoNoteRecorder.swift || {
echo "AVCaptureSession start/stop must run outside the main actor"
exit 1
}
grep -q 'afterMinimumDuration: VideoNoteStopPolicy.minimumCaptureDuration' \
Sources/Shared/UI/VideoNoteCaptureView.swift || {
echo "The dedicated video-note stop button must preserve the minimum valid recording duration"
exit 1
}
if grep -q '450_000_000\|afterMinimumDuration: 0.45' Sources/Shared/UI/ChatView.swift; then
echo "Video-note stop must follow AVCapture didStart rather than a guessed UI delay"
exit 1
fi
grep -q 'VideoNoteRecordingCompletionPolicy.shouldKeepOutput' Sources/Shared/Services/VideoNoteRecorder.swift || {
echo "Successful AVFoundation video files must survive non-nil completion errors"
exit 1
}
grep -q 'VideoNoteRecordingLifecycle.acceptsCompletion' Sources/Shared/Services/VideoNoteRecorder.swift || {
echo "Stale AVFoundation callbacks must not reset a newer video note"
exit 1
}
grep -q 'model.setVideoNoteCaptureActive(true)' Sources/Shared/UI/ChatView.swift || {
echo "Video-note capture must pause competing MAM work"
exit 1
}
grep -q 'releaseArchiveSyncAfterCapture' Sources/Shared/UI/ChatView.swift || {
echo "MAM must stay paused until video-note upload has yielded the main actor"
exit 1
}
grep -q 'ComposerRecordingGesturePolicy.isLongPress' Sources/Shared/UI/ChatView.swift || {
echo "Recording long-press must survive a delayed main actor"
exit 1
}
grep -q 'beginPhysicalPress(at: value.time)' Sources/Shared/UI/ChatView.swift || {
echo "Recording duration must use physical gesture timestamps"
exit 1
}
grep -q 'ComposerRecordTouchSurface' Sources/Shared/UI/ChatView.swift || {
echo "iOS voice recording must retain the physical touch across SwiftUI updates"
exit 1
}
grep -q 'override func touchesCancelled' Sources/Shared/UI/ChatView.swift || {
echo "A cancelled iOS recording touch must not be treated as release-to-send"
exit 1
}
grep -q 'remainingRecordedDuration' Sources/Shared/Services/VideoNoteRecorder.swift || {
echo "Video-note minimum duration must use recorded media time"
exit 1
}
grep -q 'wasRequestedOrReachedLimit' Sources/Shared/Services/VideoNoteRecorder.swift || {
echo "Unexpected camera completion must not send a partial video note"
exit 1
}
grep -q 'normalizedRecordingPath' Sources/Shared/Models/VideoNoteRecordingLifecycle.swift || {
echo "macOS temporary-path aliases must identify the active recording"
exit 1
}
grep -q 'AVVideoCodecType.h264' Sources/Shared/Services/VideoNoteRecorder.swift || {
echo "Video notes must use the interoperable H.264 codec when available"
exit 1
}
grep -q 'VideoNoteFileInspector' Sources/Shared/Services/VideoNoteRecorder.swift || {
echo "Finalized video notes must be inspected before upload"
exit 1
}
grep -q 'UIImagePickerController' Sources/Shared/UI/SystemPhotoCameraView.swift || {
echo "Native iPhone camera capture is missing"
exit 1
}
grep -q 'picker.mediaTypes = \[UTType.image.identifier, UTType.movie.identifier\]' \
Sources/Shared/UI/SystemPhotoCameraView.swift || {
echo "The native camera must support both photos and videos"
exit 1
}
grep -q 'Label("Камера"' Sources/Shared/UI/ChatView.swift || {
echo "The attachment menu must expose the photo/video camera"
exit 1
}
grep -q 'presentAttachmentPreviewAfterPickerDismissal' Sources/Shared/UI/ChatView.swift || {
echo "PhotosPicker dismissal must complete before attachment preview presentation"
exit 1
}
grep -q 'attachmentPreviewPresentationPending' Sources/Shared/UI/ChatView.swift || {
echo "A prepared attachment preview must remain queued across picker dismissal"
exit 1
}
if sed -n '/private func presentAttachmentPreviewAfterPickerDismissal/,/private func loadPickedMediaFile/p' \
Sources/Shared/UI/ChatView.swift | grep -q 'showingMediaPicker = false'; then
echo "Attachment preview must not force-close PhotosPicker while UIKit is dismissing it"
exit 1
fi
if sed -n '/private func copyPickedMediaFile/,/^}/p' Sources/Shared/UI/ChatView.swift \
| grep -q 'options: \.forUploading'; then
echo "PhotosPicker files must be coordinated as files, not upload packages"
exit 1
fi
grep -q 'case reaction(ReactionEnvelope)' Sources/Shared/XMPP/XMPPService.swift || {
echo "XEP-0444 reaction events are missing"
exit 1
}
grep -q 'urn:xmpp:reactions:0' Sources/Shared/XMPP/XMPPService.swift || {
echo "XEP-0444 capability advertisement is missing"
exit 1
}
grep -q 'MessageReactionPolicy.quickChoices' Sources/Shared/UI/Components/MessageBubble.swift || {
echo "Message reaction UI is missing"
exit 1
}
grep -q 'ChatStateNotificationsModule' Sources/Shared/XMPP/XMPPService.swift || {
echo "XEP-0085 chat-state support is missing"
exit 1
}
grep -q 'model.updateComposerActivity' Sources/Shared/UI/ChatView.swift || {
echo "The composer is not connected to typing notifications"
exit 1
}
grep -q 'snapshotQueue.async' Sources/Shared/Services/PhoneWatchBridge.swift || {
echo "Apple Watch snapshot encoding must not block SwiftUI"
exit 1
}
awk '
/^#if os\(iOS\)$/ { ios_only = 1; next }
/^#endif$/ && ios_only { ios_only = 0; next }
/availableVideoCodecTypes/ {
found = 1
if (!ios_only) escaped = 1
}
END { exit !(found && !escaped) }
' Sources/Shared/Services/VideoNoteRecorder.swift || {
echo "iOS-only movie codec discovery escaped its platform guard"
exit 1
}
grep -q 'canRetryMediaMessage' Sources/Shared/UI/Components/MessageBubble.swift || {
echo "Failed media messages must expose a retry action"
exit 1
}
grep -q 'MediaViewerDismissGesturePolicy.shouldDismiss' Sources/Shared/UI/Components/MediaViewer.swift || {
echo "Full-screen media vertical swipe dismissal is missing"
exit 1
}
if grep -q 'Image(systemName: "xmark")' Sources/Shared/UI/Components/MediaViewer.swift; then
echo "Full-screen media viewer must not show a close button"
exit 1
fi
grep -q 'selectedMessageIDs' Sources/Shared/UI/ChatView.swift || {
echo "Multiple message selection is missing"
exit 1
}
grep -q 'forwardMessages' Sources/Shared/UI/ForwardMessageView.swift || {
echo "Multiple message forwarding is missing"
exit 1
}
grep -q 'MediaSendActivityTracker' Sources/Shared/Models/AppModel.swift || {
echo "Overlapping media send recovery is missing"
exit 1
}
grep -q 'failedDrafts' Sources/Shared/UI/AttachmentPreviewView.swift || {
echo "Failed media drafts must remain available for retry"
exit 1
}
grep -q 'rosterContactJIDs' Sources/Shared/UI/MainChatView.swift || {
echo "Prosody roster contacts screen is missing"
exit 1
}
grep -q '\[.banner, .list, .sound\]' Sources/Shared/Services/NotificationCoordinator.swift || {
echo "Foreground notification presentation is missing"
exit 1
}
for catalog in Resources/*.xcassets/AppIcon.appiconset/Contents.json; do
python3 -m json.tool "$catalog" >/dev/null
python3 - "$catalog" <<'PY'
import json
import pathlib
import sys
catalog = pathlib.Path(sys.argv[1])
contents = json.loads(catalog.read_text())
filenames = [item.get("filename") for item in contents.get("images", [])]
if not filenames or any(not name for name in filenames):
raise SystemExit(f"Incomplete AppIcon catalog: {catalog}")
missing = [name for name in filenames if not (catalog.parent / name).is_file()]
if missing:
raise SystemExit(f"Missing AppIcon files in {catalog}: {', '.join(missing)}")
PY
done
for plist in Config/Luma-Info.plist Config/LumaMac-Info.plist Config/LumaWatch-Info.plist; do
grep -q '<key>CFBundleIconName</key>' "$plist" || { echo "AppIcon is not configured in $plist"; exit 1; }
done
if command -v plutil >/dev/null; then
plutil -lint Config/*.plist Config/*.entitlements >/dev/null
fi
if command -v xcodegen >/dev/null; then
xcodegen generate --quiet
fi
if command -v xcodebuild >/dev/null && test -d Luma.xcodeproj; then
xcodebuild -project Luma.xcodeproj -scheme Luma \
-destination 'generic/platform=iOS Simulator' CODE_SIGNING_ALLOWED=NO build
else
echo "Static project checks passed. Run on macOS with Xcode for a full build."
fi