diff --git a/.drone.jsonnet b/.drone.jsonnet new file mode 100644 index 000000000..1377b86f4 --- /dev/null +++ b/.drone.jsonnet @@ -0,0 +1,132 @@ +// Intentionally doing a depth of 2 as libSession-util has it's own submodules (and libLokinet likely will as well) +local clone_submodules = { + name: 'Clone Submodules', + commands: ['git fetch --tags', 'git submodule update --init --recursive --depth=2'] +}; + +// cmake options for static deps mirror +local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https://oxen.rocks/deps ' else ''); + +// Cocoapods +// +// Unfortunately Cocoapods has a dumb restriction which requires you to use UTF-8 for the +// 'LANG' env var so we need to work around the with https://github.com/CocoaPods/CocoaPods/issues/6333 +local install_cocoapods = { + name: 'Install CocoaPods', + commands: ['LANG=en_US.UTF-8 pod install'] +}; + +// Load from the cached CocoaPods directory (to speed up the build) +local load_cocoapods_cache = { + name: 'Load CocoaPods Cache', + commands: [ + ||| + while test -e /Users/drone/.cocoapods_cache.lock; do + sleep 1 + done + |||, + 'touch /Users/drone/.cocoapods_cache.lock', + ||| + if [[ -d /Users/drone/.cocoapods_cache ]]; then + cp -r /Users/drone/.cocoapods_cache ./Pods + fi + |||, + 'rm /Users/drone/.cocoapods_cache.lock' + ] +}; + +// Override the cached CocoaPods directory (to speed up the next build) +local update_cocoapods_cache = { + name: 'Update CocoaPods Cache', + commands: [ + ||| + while test -e /Users/drone/.cocoapods_cache.lock; do + sleep 1 + done + |||, + 'touch /Users/drone/.cocoapods_cache.lock', + ||| + if [[ -d ./Pods ]]; then + rm -rf /Users/drone/.cocoapods_cache + cp -r ./Pods /Users/drone/.cocoapods_cache + fi + |||, + 'rm /Users/drone/.cocoapods_cache.lock' + ] +}; + + +[ + // Unit tests + { + kind: 'pipeline', + type: 'exec', + name: 'Unit Tests', + platform: { os: 'darwin', arch: 'amd64' }, + steps: [ + clone_submodules, + load_cocoapods_cache, + install_cocoapods, + { + name: 'Run Unit Tests', + commands: [ + 'mkdir build', + 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -workspace Session.xcworkspace -scheme Session -destination "platform=iOS Simulator,name=iPhone 14 Pro" -maximum-test-execution-time-allowance 2 -collect-test-diagnostics never 2>&1 | ./Pods/xcbeautify/xcbeautify' + ], + }, + update_cocoapods_cache + ], + }, + // Simulator build + { + kind: 'pipeline', + type: 'exec', + name: 'Simulator Build', + platform: { os: 'darwin', arch: 'amd64' }, + steps: [ + clone_submodules, + load_cocoapods_cache, + install_cocoapods, + { + name: 'Build', + commands: [ + 'mkdir build', + 'xcodebuild archive -workspace Session.xcworkspace -scheme Session -configuration "App Store Release" -sdk iphonesimulator -archivePath ./build/Session_sim.xcarchive -destination "generic/platform=iOS Simulator" | ./Pods/xcbeautify/xcbeautify' + ], + }, + update_cocoapods_cache, + { + name: 'Upload artifacts', + commands: [ + './Scripts/drone-static-upload.sh' + ] + }, + ], + }, + // AppStore build (generate an archive to be signed later) + { + kind: 'pipeline', + type: 'exec', + name: 'AppStore Build', + platform: { os: 'darwin', arch: 'amd64' }, + steps: [ + clone_submodules, + load_cocoapods_cache, + install_cocoapods, + { + name: 'Build', + commands: [ + 'mkdir build', + 'xcodebuild archive -workspace Session.xcworkspace -scheme Session -configuration "App Store Release" -sdk iphoneos -archivePath ./build/Session.xcarchive -destination "generic/platform=iOS" -allowProvisioningUpdates | ./Pods/xcbeautify/xcbeautify' + ], + }, + update_cocoapods_cache, + { + name: 'Upload artifacts', + commands: [ + './Scripts/drone-static-upload.sh' + ] + }, + ], + }, +] \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index ae3d45a8c..b8c1e3809 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "LibSession-Util"] path = LibSession-Util - url = git@github.com:oxen-io/libsession-util.git + url = https://github.com/oxen-io/libsession-util.git diff --git a/LibSession-Util b/LibSession-Util index e0b994201..7eb870283 160000 --- a/LibSession-Util +++ b/LibSession-Util @@ -1 +1 @@ -Subproject commit e0b994201a016cc5bf9065526a0ceb4291f60d5a +Subproject commit 7eb87028355bfc89950102c52d5b2927a25b2e22 diff --git a/Podfile b/Podfile index 25f76e8e9..201db8853 100644 --- a/Podfile +++ b/Podfile @@ -1,9 +1,13 @@ platform :ios, '13.0' -source 'https://github.com/CocoaPods/Specs.git' use_frameworks! inhibit_all_warnings! +install! 'cocoapods', :warn_for_unused_master_specs_repo => false + +# CI Dependencies +pod 'xcbeautify' + # Dependencies to be included in the app and all extensions/frameworks abstract_target 'GlobalDependencies' do # FIXME: If https://github.com/jedisct1/swift-sodium/pull/249 gets resolved then revert this back to the standard pod @@ -22,7 +26,6 @@ abstract_target 'GlobalDependencies' do pod 'PureLayout', '~> 3.1.8' pod 'NVActivityIndicatorView' pod 'YYImage/libwebp', git: 'https://github.com/signalapp/YYImage' - pod 'ZXingObjC' pod 'DifferenceKit' target 'SessionTests' do @@ -101,7 +104,6 @@ end # Actions to perform post-install post_install do |installer| set_minimum_deployment_target(installer) - avoid_rsync_webrtc_if_unchanged(installer) end def set_minimum_deployment_target(installer) @@ -111,12 +113,3 @@ def set_minimum_deployment_target(installer) end end end - -# This function patches the Cocoapods 'Embed Frameworks' script to avoid running rsync -# for the WebRTC-lib framework in simulator builds if it has already been copied over -# because due to the size it can take over 10 seconds to embed, and gets embeded in -# each target on every build regardless of whether there were changes, drastically -# increasing the length of the build -def avoid_rsync_webrtc_if_unchanged(installer) - system('find "./Pods/Target Support Files" -name "*-frameworks.sh" -exec patch -p0 -i ./Scripts/skip_web_rtc_re_rsync.patch {} \;') -end diff --git a/Podfile.lock b/Podfile.lock index d44b761b9..4a101f497 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -41,7 +41,8 @@ PODS: - SQLCipher/standard (4.5.3): - SQLCipher/common - SwiftProtobuf (1.5.0) - - WebRTC-lib (96.0.0) + - WebRTC-lib (114.0.0) + - xcbeautify (0.17.0) - YapDatabase/SQLCipher (3.1.1): - YapDatabase/SQLCipher/Core (= 3.1.1) - YapDatabase/SQLCipher/Extensions (= 3.1.1) @@ -108,9 +109,6 @@ PODS: - YYImage/libwebp (1.0.4): - libwebp - YYImage/Core - - ZXingObjC (3.6.5): - - ZXingObjC/All (= 3.6.5) - - ZXingObjC/All (3.6.5) DEPENDENCIES: - Curve25519Kit (from `https://github.com/oxen-io/session-ios-curve-25519-kit.git`, branch `session-version`) @@ -127,9 +125,9 @@ DEPENDENCIES: - SQLCipher (~> 4.5.3) - SwiftProtobuf (~> 1.5.0) - WebRTC-lib + - xcbeautify - YapDatabase/SQLCipher (from `https://github.com/oxen-io/session-ios-yap-database.git`, branch `signal-release`) - YYImage/libwebp (from `https://github.com/signalapp/YYImage`) - - ZXingObjC SPEC REPOS: https://github.com/CocoaPods/Specs.git: @@ -147,7 +145,8 @@ SPEC REPOS: - SQLCipher - SwiftProtobuf - WebRTC-lib - - ZXingObjC + trunk: + - xcbeautify EXTERNAL SOURCES: Curve25519Kit: @@ -199,11 +198,11 @@ SPEC CHECKSUMS: Sodium: a7d42cb46e789d2630fa552d35870b416ed055ae SQLCipher: 57fa9f863fa4a3ed9dd3c90ace52315db8c0fdca SwiftProtobuf: 241400280f912735c1e1b9fe675fdd2c6c4d42e2 - WebRTC-lib: 508fe02efa0c1a3a8867082a77d24c9be5d29aeb + WebRTC-lib: d83df8976fa608b980f1d85796b3de66d60a1953 + xcbeautify: 6e2f57af5c3a86d490376d5758030a8dcc201c1b YapDatabase: b418a4baa6906e8028748938f9159807fd039af4 YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331 - ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb -PODFILE CHECKSUM: dcca0c4ad69b14cbc2d6ba49f9d690b239828e6d +PODFILE CHECKSUM: dd814a5a92577bb2a94dac6a1cc482f193721cdf COCOAPODS: 1.12.1 diff --git a/Scripts/LintLocalizableStrings.swift b/Scripts/LintLocalizableStrings.swift index e22c99900..d048d196b 100755 --- a/Scripts/LintLocalizableStrings.swift +++ b/Scripts/LintLocalizableStrings.swift @@ -26,6 +26,7 @@ var pathFiles: [String] = { return fileUrls .filter { ((try? $0.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == false) && // No directories + !$0.path.contains("build/") && // Exclude files under the build folder (CI) !$0.path.contains("Pods/") && // Exclude files under the pods folder !$0.path.contains(".xcassets") && // Exclude asset bundles !$0.path.contains(".app/") && // Exclude files in the app build directories diff --git a/Scripts/build_libSession_util.sh b/Scripts/build_libSession_util.sh index 998c8f462..c32606503 100755 --- a/Scripts/build_libSession_util.sh +++ b/Scripts/build_libSession_util.sh @@ -27,37 +27,41 @@ # Need to set the path or we won't find cmake PATH=${PATH}:/usr/local/bin:/opt/homebrew/bin:/sbin/md5 -SHOULD_AUTO_INIT_SUBMODULES=${1:-false} + +exec 3>&1 # Save original stdout + +# Ensure the build directory exists (in case we need it before XCode creates it) +mkdir -p "${TARGET_BUILD_DIR}/libSessionUtil" + +# Remove any old build errors +rm -rf "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_output.log" + +# Restore stdout and stderr and redirect it to the 'libsession_util_output.log' file +exec &> "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_output.log" + +# Define a function to echo a message. +function echo_message() { + exec 1>&3 # Restore stdout + echo "$1" + exec >> "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_output.log" # Redirect all output to the log file +} + +echo_message "info: Validating build requirements" + +set -x # Ensure the build directory exists (in case we need it before XCode creates it) mkdir -p "${TARGET_BUILD_DIR}" -# Remove any old build errors -rm -rf "${TARGET_BUILD_DIR}/libsession_util_error.log" - -# First ensure cmake is installed (store the error in a log and exit with a success status - xcode will output the error) -echo "info: Validating build requirements" - if ! which cmake > /dev/null; then - touch "${TARGET_BUILD_DIR}/libsession_util_error.log" - echo "error: cmake is required to build, please install (can install via homebrew with 'brew install cmake')." - echo "error: cmake is required to build, please install (can install via homebrew with 'brew install cmake')." > "${TARGET_BUILD_DIR}/libsession_util_error.log" + echo_message "error: cmake is required to build, please install (can install via homebrew with 'brew install cmake')." exit 0 fi # Check if we have the `LibSession-Util` submodule checked out and if not (depending on the 'SHOULD_AUTO_INIT_SUBMODULES' argument) perform the checkout if [ ! -d "${SRCROOT}/LibSession-Util" ] || [ ! -d "${SRCROOT}/LibSession-Util/src" ] || [ ! "$(ls -A "${SRCROOT}/LibSession-Util")" ]; then - if [ "${SHOULD_AUTO_INIT_SUBMODULES}" != "false" ] & command -v git >/dev/null 2>&1; then - echo "info: LibSession-Util submodule doesn't exist, resetting and checking out recusively now" - git submodule foreach --recursive git reset --hard - git submodule update --init --recursive - echo "info: Checkout complete" - else - touch "${TARGET_BUILD_DIR}/libsession_util_error.log" - echo "error: Need to fetch LibSession-Util submodule (git submodule update --init --recursive)." - echo "error: Need to fetch LibSession-Util submodule (git submodule update --init --recursive)." > "${TARGET_BUILD_DIR}/libsession_util_error.log" - exit 0 - fi + echo_message "error: Need to fetch LibSession-Util submodule (git submodule update --init --recursive)." + exit 0 else are_submodules_valid() { local PARENT_PATH=$1 @@ -82,7 +86,7 @@ else # If the child path doesn't exist then it's invalid if [ ! -d "${PARENT_PATH}/${CHILD_PATH}" ]; then - echo "info: Submodule '${RELATIVE_PATH}/${CHILD_PATH}' doesn't exist." + echo_message "info: Submodule '${RELATIVE_PATH}/${CHILD_PATH}' doesn't exist." return 1 fi @@ -90,7 +94,7 @@ else local RESULT=$? if [ "${RESULT}" -eq 1 ]; then - echo "info: Submodule '${RELATIVE_PATH}/${CHILD_PATH}' is in an invalid state." + echo_message "info: Submodule '${RELATIVE_PATH}/${CHILD_PATH}' is in an invalid state." return 1 fi done @@ -104,18 +108,8 @@ else HAS_INVALID_SUBMODULE=$? if [ "${HAS_INVALID_SUBMODULE}" -eq 1 ]; then - if [ "${SHOULD_AUTO_INIT_SUBMODULES}" != "false" ] && command -v git >/dev/null 2>&1; then - echo "info: Submodules are in an invalid state, resetting and checking out recusively now" - cd "${SRCROOT}/LibSession-Util" - git submodule foreach --recursive git reset --hard - git submodule update --init --recursive - echo "info: Checkout complete" - else - touch "${TARGET_BUILD_DIR}/libsession_util_error.log" - echo "error: Submodules are in an invalid state, please delete 'LibSession-Util' and run 'git submodule update --init --recursive'." - echo "error: Submodules are in an invalid state, please delete 'LibSession-Util' and run 'git submodule update --init --recursive'." > "${TARGET_BUILD_DIR}/libsession_util_error.log" - exit 0 - fi + echo_message "error: Submodules are in an invalid state, please delete 'LibSession-Util' and run 'git submodule update --init --recursive'." + exit 0 fi fi @@ -125,49 +119,143 @@ echo "info: Checking for changes to source" NEW_SOURCE_HASH=$(find "${SRCROOT}/LibSession-Util/src" -type f -exec md5 {} + | awk '{print $NF}' | sort | md5 | awk '{print $NF}') NEW_HEADER_HASH=$(find "${SRCROOT}/LibSession-Util/include" -type f -exec md5 {} + | awk '{print $NF}' | sort | md5 | awk '{print $NF}') -if [ -f "${TARGET_BUILD_DIR}/libsession_util_source_hash.log" ]; then - read -r OLD_SOURCE_HASH < "${TARGET_BUILD_DIR}/libsession_util_source_hash.log" +if [ -f "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_source_hash.log" ]; then + read -r OLD_SOURCE_HASH < "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_source_hash.log" fi -if [ -f "${TARGET_BUILD_DIR}/libsession_util_header_hash.log" ]; then - read -r OLD_HEADER_HASH < "${TARGET_BUILD_DIR}/libsession_util_header_hash.log" +if [ -f "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_header_hash.log" ]; then + read -r OLD_HEADER_HASH < "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_header_hash.log" fi -if [ -f "${TARGET_BUILD_DIR}/libsession_util_archs.log" ]; then - read -r OLD_ARCHS < "${TARGET_BUILD_DIR}/libsession_util_archs.log" +if [ -f "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_archs.log" ]; then + read -r OLD_ARCHS < "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_archs.log" fi -# Start the libSession-util build if it doesn't already exists -if [ "${NEW_SOURCE_HASH}" != "${OLD_SOURCE_HASH}" ] || [ "${NEW_HEADER_HASH}" != "${OLD_HEADER_HASH}" ] || [ "${ARCHS[*]}" != "${OLD_ARCHS}" ] || [ ! -d "${TARGET_BUILD_DIR}/libsession-util.xcframework" ]; then - echo "info: Build is not up-to-date - creating new build" - echo "" - - # Remove any existing build files (just to be safe) - rm -rf "${TARGET_BUILD_DIR}/libsession-util.a" - rm -rf "${TARGET_BUILD_DIR}/libsession-util.xcframework" - rm -rf "${BUILD_DIR}/libsession-util.xcframework" - - # Trigger the new build - cd "${SRCROOT}/LibSession-Util" - result=$(./utils/ios.sh "libsession-util" false) - - if [ $? -ne 0 ]; then - touch "${TARGET_BUILD_DIR}/libsession_util_error.log" - echo "error: Failed to build libsession-util (See details in '${TARGET_BUILD_DIR}/pre-action-output.log')." - echo "error: Failed to build libsession-util (See details in '${TARGET_BUILD_DIR}/pre-action-output.log')." > "${TARGET_BUILD_DIR}/libsession_util_error.log" - exit 0 - fi - - # Save the updated source hash to disk to prevent rebuilds when there were no changes - echo "${NEW_SOURCE_HASH}" > "${TARGET_BUILD_DIR}/libsession_util_source_hash.log" - echo "${NEW_HEADER_HASH}" > "${TARGET_BUILD_DIR}/libsession_util_header_hash.log" - echo "${ARCHS[*]}" > "${TARGET_BUILD_DIR}/libsession_util_archs.log" - echo "" - echo "info: Build complete" -else - echo "info: Build is up-to-date" +# If all of the hashes match, the archs match and there is a library file then we can just stop here +if [ "${NEW_SOURCE_HASH}" == "${OLD_SOURCE_HASH}" ] && [ "${NEW_HEADER_HASH}" == "${OLD_HEADER_HASH}" ] && [ "${ARCHS[*]}" == "${OLD_ARCHS}" ] && [ -f "${TARGET_BUILD_DIR}/libSessionUtil/libSessionUtil.a" ]; then + echo_message "info: Build is up-to-date" + exit 0 fi -# Move the target-specific libSession-util build to the parent build directory (so XCode can have a reference to a single build) -rm -rf "${BUILD_DIR}/libsession-util.xcframework" -cp -r "${TARGET_BUILD_DIR}/libsession-util.xcframework" "${BUILD_DIR}/libsession-util.xcframework" +# If any of the above differ then we need to rebuild +echo_message "info: Build is not up-to-date - creating new build" + +# Import settings from XCode (defaulting values if not present) +VALID_SIM_ARCHS=(arm64 x86_64) +VALID_DEVICE_ARCHS=(arm64) +VALID_SIM_ARCH_PLATFORMS=(SIMULATORARM64 SIMULATOR64) +VALID_DEVICE_ARCH_PLATFORMS=(OS64) + +OUTPUT_DIR="${TARGET_BUILD_DIR}" +IPHONEOS_DEPLOYMENT_TARGET=${IPHONEOS_DEPLOYMENT_TARGET} +ENABLE_BITCODE=${ENABLE_BITCODE} + +# Generate the target architectures we want to build for +TARGET_ARCHS=() +TARGET_PLATFORMS=() +TARGET_SIM_ARCHS=() +TARGET_DEVICE_ARCHS=() + +if [ -z $PLATFORM_NAME ] || [ $PLATFORM_NAME = "iphonesimulator" ]; then + for i in "${!VALID_SIM_ARCHS[@]}"; do + ARCH="${VALID_SIM_ARCHS[$i]}" + ARCH_PLATFORM="${VALID_SIM_ARCH_PLATFORMS[$i]}" + + if [[ " ${ARCHS[*]} " =~ " ${ARCH} " ]]; then + TARGET_ARCHS+=("sim-${ARCH}") + TARGET_PLATFORMS+=("${ARCH_PLATFORM}") + TARGET_SIM_ARCHS+=("sim-${ARCH}") + fi + done +fi + +if [ -z $PLATFORM_NAME ] || [ $PLATFORM_NAME = "iphoneos" ]; then + for i in "${!VALID_DEVICE_ARCHS[@]}"; do + ARCH="${VALID_DEVICE_ARCHS[$i]}" + ARCH_PLATFORM="${VALID_DEVICE_ARCH_PLATFORMS[$i]}" + + if [[ " ${ARCHS[*]} " =~ " ${ARCH} " ]]; then + TARGET_ARCHS+=("ios-${ARCH}") + TARGET_PLATFORMS+=("${ARCH_PLATFORM}") + TARGET_DEVICE_ARCHS+=("ios-${ARCH}") + fi + done +fi + +# Build the individual architectures +for i in "${!TARGET_ARCHS[@]}"; do + build="${TARGET_BUILD_DIR}/libSessionUtil/${TARGET_ARCHS[$i]}" + platform="${TARGET_PLATFORMS[$i]}" + echo_message "Building ${TARGET_ARCHS[$i]} for $platform in $build" + + cd "${SRCROOT}/LibSession-Util" + ./utils/static-bundle.sh "$build" "" \ + -DCMAKE_TOOLCHAIN_FILE="${SRCROOT}/LibSession-Util/external/ios-cmake/ios.toolchain.cmake" \ + -DPLATFORM=$platform \ + -DDEPLOYMENT_TARGET=$IPHONEOS_DEPLOYMENT_TARGET \ + -DENABLE_BITCODE=$ENABLE_BITCODE + + if [ $? -ne 0 ]; then + LAST_OUTPUT=$(tail -n 4 "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_output.log" | head -n 1) + echo_message "error: $LAST_OUTPUT" + exit 1 + fi +done + +# Remove the old static library file +rm -rf "${TARGET_BUILD_DIR}/libSessionUtil/libSessionUtil.a" +rm -rf "${TARGET_BUILD_DIR}/libSessionUtil/Headers" + +# If needed combine simulator builds into a multi-arch lib +if [ "${#TARGET_SIM_ARCHS[@]}" -eq "1" ]; then + # Single device build + cp "${TARGET_BUILD_DIR}/libSessionUtil/${TARGET_SIM_ARCHS[0]}/libsession-util.a" "${TARGET_BUILD_DIR}/libSessionUtil/libSessionUtil.a" +elif [ "${#TARGET_SIM_ARCHS[@]}" -gt "1" ]; then + # Combine multiple device builds into a multi-arch lib + echo_message "info: Built multiple architectures, merging into single static library" + lipo -create "${TARGET_BUILD_DIR}/libSessionUtil"/sim-*/libsession-util.a -output "${TARGET_BUILD_DIR}/libSessionUtil/libSessionUtil.a" +fi + +# If needed combine device builds into a multi-arch lib +if [ "${#TARGET_DEVICE_ARCHS[@]}" -eq "1" ]; then + cp "${TARGET_BUILD_DIR}/libSessionUtil/${TARGET_DEVICE_ARCHS[0]}/libsession-util.a" "${TARGET_BUILD_DIR}/libSessionUtil/libSessionUtil.a" +elif [ "${#TARGET_DEVICE_ARCHS[@]}" -gt "1" ]; then + # Combine multiple device builds into a multi-arch lib + echo_message "info: Built multiple architectures, merging into single static library" + lipo -create "${TARGET_BUILD_DIR}/libSessionUtil"/ios-*/libsession-util.a -output "${TARGET_BUILD_DIR}/libSessionUtil/libSessionUtil.a" +fi + +# Save the updated hashes to disk to prevent rebuilds when there were no changes +echo "${NEW_SOURCE_HASH}" > "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_source_hash.log" +echo "${NEW_HEADER_HASH}" > "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_header_hash.log" +echo "${ARCHS[*]}" > "${TARGET_BUILD_DIR}/libSessionUtil/libsession_util_archs.log" +echo_message "info: Build complete" + +# Copy the headers across +echo_message "info: Copy headers and prepare modulemap" +mkdir -p "${TARGET_BUILD_DIR}/libSessionUtil/Headers" +cp -r "${SRCROOT}/LibSession-Util/include/session" "${TARGET_BUILD_DIR}/libSessionUtil/Headers" + +# The 'module.modulemap' is needed for XCode to be able to find the headers +modmap="${TARGET_BUILD_DIR}/libSessionUtil/Headers/module.modulemap" +echo "module SessionUtil {" >"$modmap" +echo " module capi {" >>"$modmap" +for x in $(cd include && find session -name '*.h'); do + echo " header \"$x\"" >>"$modmap" +done +echo -e " export *\n }" >>"$modmap" +if false; then + # If we include the cpp headers like this then Xcode will try to load them as C headers (which + # of course breaks) and doesn't provide any way to only load the ones you need (because this is + # Apple land, why would anything useful be available?). So we include the headers in the + # archive but can't let xcode discover them because it will do it wrong. + echo -e "\n module cppapi {" >>"$modmap" + for x in $(cd include && find session -name '*.hpp'); do + echo " header \"$x\"" >>"$modmap" + done + echo -e " export *\n }" >>"$modmap" +fi +echo "}" >>"$modmap" + +# Output to XCode just so the output is good +echo_message "info: libSessionUtil Ready" \ No newline at end of file diff --git a/Scripts/drone-static-upload.sh b/Scripts/drone-static-upload.sh new file mode 100755 index 000000000..4fd13faa5 --- /dev/null +++ b/Scripts/drone-static-upload.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +# Script used with Drone CI to upload build artifacts (because specifying all this in +# .drone.jsonnet is too painful). + + + +set -o errexit + +if [ -z "$SSH_KEY" ]; then + echo -e "\n\n\n\e[31;1mUnable to upload artifact: SSH_KEY not set\e[0m" + # Just warn but don't fail, so that this doesn't trigger a build failure for untrusted builds + exit 0 +fi + +echo "$SSH_KEY" >ssh_key + +set -o xtrace # Don't start tracing until *after* we write the ssh key + +chmod 600 ssh_key + +if [ -n "$DRONE_TAG" ]; then + # For a tag build use something like `session-ios-v1.2.3` + base="session-ios-$DRONE_TAG" +else + # Otherwise build a length name from the datetime and commit hash, such as: + # session-ios-20200522T212342Z-04d7dcc54 + base="session-ios-$(date --date=@$DRONE_BUILD_CREATED +%Y%m%dT%H%M%SZ)-${DRONE_COMMIT:0:9}" +fi + +mkdir -v "$base" + +# Copy over the build products +prod_path="build/Session.xcarchive" +sim_path="build/Session_sim.xcarchive/Products/Applications/Session.app" + +mkdir build +echo "Test" > "build/test.txt" + +if [ ! -d $prod_path ]; then + cp -av $prod_path "$base" +else if [ ! -d $sim_path ]; then + cp -av $sim_path "$base" +else + echo "Expected a file to upload, found none" >&2 + exit 1 +fi + +# tar dat shiz up yo +archive="$base.tar.xz" +tar cJvf "$archive" "$base" + +upload_to="oxen.rocks/${DRONE_REPO// /_}/${DRONE_BRANCH// /_}" + +# sftp doesn't have any equivalent to mkdir -p, so we have to split the above up into a chain of +# -mkdir a/, -mkdir a/b/, -mkdir a/b/c/, ... commands. The leading `-` allows the command to fail +# without error. +upload_dirs=(${upload_to//\// }) +put_debug= +mkdirs= +dir_tmp="" +for p in "${upload_dirs[@]}"; do + dir_tmp="$dir_tmp$p/" + mkdirs="$mkdirs +-mkdir $dir_tmp" +done + +sftp -i ssh_key -b - -o StrictHostKeyChecking=off drone@oxen.rocks < /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - FD7692EA2A524303000E4B70 /* Validate pre-build actions */ = { + FD9BDDFF2A5D229B005F1EBC /* Build libSessionUtil if Needed */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; @@ -5384,198 +5362,19 @@ ); inputPaths = ( ); - name = "Validate pre-build actions"; + name = "Build libSessionUtil if Needed"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ -f \"${TARGET_BUILD_DIR}/libsession_util_error.log\" ]; then\n read -r line < \"${TARGET_BUILD_DIR}/libsession_util_error.log\"\n echo \"${line}\"\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; - FD7692EC2A524320000E4B70 /* Validate pre-build actions */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Validate pre-build actions"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ -f \"${TARGET_BUILD_DIR}/libsession_util_error.log\" ]; then\n read -r line < \"${TARGET_BUILD_DIR}/libsession_util_error.log\"\n echo \"${line}\"\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; - FD7692ED2A52433E000E4B70 /* Validate pre-build actions */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Validate pre-build actions"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ -f \"${TARGET_BUILD_DIR}/libsession_util_error.log\" ]; then\n read -r line < \"${TARGET_BUILD_DIR}/libsession_util_error.log\"\n echo \"${line}\"\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; - FD7692EE2A524357000E4B70 /* Validate pre-build actions */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Validate pre-build actions"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ -f \"${TARGET_BUILD_DIR}/libsession_util_error.log\" ]; then\n read -r line < \"${TARGET_BUILD_DIR}/libsession_util_error.log\"\n echo \"${line}\"\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; - FD7692EF2A52436A000E4B70 /* Validate pre-build actions */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Validate pre-build actions"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ -f \"${TARGET_BUILD_DIR}/libsession_util_error.log\" ]; then\n read -r line < \"${TARGET_BUILD_DIR}/libsession_util_error.log\"\n echo \"${line}\"\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; - FD7692F02A524393000E4B70 /* Validate pre-build actions */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Validate pre-build actions"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ -f \"${TARGET_BUILD_DIR}/libsession_util_error.log\" ]; then\n read -r line < \"${TARGET_BUILD_DIR}/libsession_util_error.log\"\n echo \"${line}\"\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; - FD7692F12A5243AE000E4B70 /* Validate pre-build actions */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Validate pre-build actions"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ -f \"${TARGET_BUILD_DIR}/libsession_util_error.log\" ]; then\n read -r line < \"${TARGET_BUILD_DIR}/libsession_util_error.log\"\n echo \"${line}\"\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; - FD7692F22A5243C3000E4B70 /* Validate pre-build actions */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Validate pre-build actions"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ -f \"${TARGET_BUILD_DIR}/libsession_util_error.log\" ]; then\n read -r line < \"${TARGET_BUILD_DIR}/libsession_util_error.log\"\n echo \"${line}\"\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; - FD7692F32A5243DA000E4B70 /* Validate pre-build actions */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Validate pre-build actions"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ -f \"${TARGET_BUILD_DIR}/libsession_util_error.log\" ]; then\n read -r line < \"${TARGET_BUILD_DIR}/libsession_util_error.log\"\n echo \"${line}\"\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; - FD7692F42A5243EC000E4B70 /* Validate pre-build actions */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Validate pre-build actions"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ -f \"${TARGET_BUILD_DIR}/libsession_util_error.log\" ]; then\n read -r line < \"${TARGET_BUILD_DIR}/libsession_util_error.log\"\n echo \"${line}\"\n exit 1\nfi\n"; + shellScript = "\"${SRCROOT}/Scripts/build_libSession_util.sh\"\n"; showEnvVarsInLog = 0; }; FDD82C422A2085B900425F05 /* Add Commit Hash To Build Info Plist */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -5615,26 +5414,6 @@ shellScript = "\"${SRCROOT}/Scripts/LintLocalizableStrings.swift\"\n"; showEnvVarsInLog = 0; }; - FDFC4E1729F14F7A00992FB6 /* Validate pre-build actions */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "Validate pre-build actions"; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "if [ -f \"${TARGET_BUILD_DIR}/libsession_util_error.log\" ]; then\n read -r line < \"${TARGET_BUILD_DIR}/libsession_util_error.log\"\n echo \"${line}\"\n exit 1\nfi\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -6574,16 +6353,6 @@ target = C3C2A6EF25539DE700C340D1 /* SessionMessagingKit */; targetProxy = FDC4389327B9FFC700C60D73 /* PBXContainerItemProxy */; }; - FDCDB8EC28179EAF00352A0C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = D221A088169C9E5E00537ABF /* Session */; - targetProxy = FDCDB8EB28179EAF00352A0C /* PBXContainerItemProxy */; - }; - FDCDB8EE28179EB200352A0C /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = D221A088169C9E5E00537ABF /* Session */; - targetProxy = FDCDB8ED28179EB200352A0C /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -6649,7 +6418,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 418; + CURRENT_PROJECT_VERSION = 419; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6721,7 +6490,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 418; + CURRENT_PROJECT_VERSION = 419; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -6786,7 +6555,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 418; + CURRENT_PROJECT_VERSION = 419; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; @@ -6860,7 +6629,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 418; + CURRENT_PROJECT_VERSION = 419; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = SUQ8J2PCT7; ENABLE_NS_ASSERTIONS = NO; @@ -7500,6 +7269,24 @@ GCC_OPTIMIZATION_LEVEL = 0; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack/CocoaLumberjack.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Curve25519Kit/Curve25519Kit.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GRDB.swift/GRDB.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/PureLayout/PureLayout.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Reachability/Reachability.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SAMKeychain/SAMKeychain.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SQLCipher/SQLCipher.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SignalCoreKit/SignalCoreKit.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Sodium/Sodium.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SwiftProtobuf/SwiftProtobuf.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/YapDatabase/YapDatabase.framework/Headers\"", + "\"${PODS_XCFRAMEWORKS_BUILD_DIR}/Sodium/Headers\"", + "$(PODS_ROOT)/SQLCipher", + "${SRCROOT}/LibSession-Util/include/**", + ); INFOPLIST_FILE = SessionMessagingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -7508,6 +7295,13 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + "\"${PODS_XCFRAMEWORKS_BUILD_DIR}/Sodium\"", + /usr/lib/swift, + "\"$(TARGET_BUILD_DIR)/libSessionUtil\"", + ); MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionMessagingKit"; @@ -7515,6 +7309,7 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_INCLUDE_PATHS = "$(inherited) \"${PODS_XCFRAMEWORKS_BUILD_DIR}/Clibsodium\" \"$(TARGET_BUILD_DIR)/libSessionUtil\""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -7578,6 +7373,24 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_CONFIGURATION_BUILD_DIR}/CocoaLumberjack/CocoaLumberjack.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Curve25519Kit/Curve25519Kit.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/DifferenceKit/DifferenceKit.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/GRDB.swift/GRDB.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/PureLayout/PureLayout.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Reachability/Reachability.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SAMKeychain/SAMKeychain.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SQLCipher/SQLCipher.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SignalCoreKit/SignalCoreKit.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/Sodium/Sodium.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/SwiftProtobuf/SwiftProtobuf.framework/Headers\"", + "\"${PODS_CONFIGURATION_BUILD_DIR}/YapDatabase/YapDatabase.framework/Headers\"", + "\"${PODS_XCFRAMEWORKS_BUILD_DIR}/Sodium/Headers\"", + "$(PODS_ROOT)/SQLCipher", + "${SRCROOT}/LibSession-Util/include/**", + ); INFOPLIST_FILE = SessionMessagingKit/Meta/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 13.0; @@ -7586,6 +7399,13 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}\"", + "\"${PODS_XCFRAMEWORKS_BUILD_DIR}/Sodium\"", + /usr/lib/swift, + "\"$(TARGET_BUILD_DIR)/libSessionUtil\"", + ); MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionMessagingKit"; @@ -7594,6 +7414,7 @@ SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_INCLUDE_PATHS = "$(inherited) \"${PODS_XCFRAMEWORKS_BUILD_DIR}/Clibsodium\" \"$(TARGET_BUILD_DIR)/libSessionUtil\""; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -7768,7 +7589,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 418; + CURRENT_PROJECT_VERSION = 419; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7839,7 +7660,7 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 418; + CURRENT_PROJECT_VERSION = 419; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7908,6 +7729,7 @@ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; @@ -7962,7 +7784,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; @@ -8108,6 +7930,105 @@ }; name = "App Store Release"; }; + FD9BDDFD2A5D2294005F1EBC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MODULEMAP_FILE = "$(SRCROOT)/SessionMessagingKit/Meta/SessionUtil.modulemap"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + FD9BDDFE2A5D2294005F1EBC /* App Store Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MODULEMAP_FILE = "$(SRCROOT)/SessionMessagingKit/Meta/SessionUtil.modulemap"; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = "App Store Release"; + }; FDC4389627B9FFC700C60D73 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 8727C47348B6EFA767EE583A /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionMessagingKit-SessionMessagingKitTests.debug.xcconfig */; @@ -8317,6 +8238,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = "App Store Release"; }; + FD9BDDFC2A5D2294005F1EBC /* Build configuration list for PBXNativeTarget "SessionUtil" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + FD9BDDFD2A5D2294005F1EBC /* Debug */, + FD9BDDFE2A5D2294005F1EBC /* App Store Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = "App Store Release"; + }; FDC4389527B9FFC700C60D73 /* Build configuration list for PBXNativeTarget "SessionMessagingKitTests" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme index f2e5c8744..ea85c66b2 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme @@ -5,24 +5,6 @@ - - - - - - - - - - + version = "1.3"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + version = "1.3"> - - - - - - - - - - + version = "1.3"> - - - - - - - - - - 1 && + changeset[changeset.count - 2].elementDeleted == changeset[changeset.count - 1].elementInserted + else { return false } + + let deletedModels: [MessageViewModel] = changeset[changeset.count - 2] + .elementDeleted + .map { self.viewModel.interactionData[$0.section].elements[$0.element] } + let insertedModels: [MessageViewModel] = changeset[changeset.count - 1] + .elementInserted + .map { updatedData[$0.section].elements[$0.element] } + + // Make sure all the deleted models were optimistic updates, the inserted models were not + // optimistic updates and they have the same timestamps + return ( + deletedModels.map { $0.id }.asSet() == [MessageViewModel.optimisticUpdateId] && + insertedModels.map { $0.id }.asSet() != [MessageViewModel.optimisticUpdateId] && + deletedModels.map { $0.timestampMs }.asSet() == insertedModels.map { $0.timestampMs }.asSet() + ) + }() let wasOnlyUpdates: Bool = ( - changeset.count == 1 && - changeset[0].elementUpdated.count == changeset[0].changeCount + onlyReplacedOptimisticUpdate || ( + changeset.count == 1 && + changeset[0].elementUpdated.count == changeset[0].changeCount + ) ) self.viewModel.sentMessageBeforeUpdate = false @@ -940,13 +965,13 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers guard !didSendMessageBeforeUpdate && !wasOnlyUpdates else { self.viewModel.updateInteractionData(updatedData) self.tableView.reloadData() - self.tableView.layoutIfNeeded() // If we just sent a message then we want to jump to the bottom of the conversation instantly if didSendMessageBeforeUpdate { // We need to dispatch to the next run loop because it seems trying to scroll immediately after // triggering a 'reloadData' doesn't work DispatchQueue.main.async { [weak self] in + self?.tableView.layoutIfNeeded() self?.scrollToBottom(isAnimated: false) // Note: The scroll button alpha won't get set correctly in this case so we forcibly set it to diff --git a/Session/Home/New Conversation/NewDMVC.swift b/Session/Home/New Conversation/NewDMVC.swift index 2d900083d..4b9f80591 100644 --- a/Session/Home/New Conversation/NewDMVC.swift +++ b/Session/Home/New Conversation/NewDMVC.swift @@ -165,12 +165,12 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle dismiss(animated: true, completion: nil) } - func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String) { + func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?) { let hexEncodedPublicKey = string - startNewDMIfPossible(with: hexEncodedPublicKey) + startNewDMIfPossible(with: hexEncodedPublicKey, onError: onError) } - fileprivate func startNewDMIfPossible(with onsNameOrPublicKey: String) { + fileprivate func startNewDMIfPossible(with onsNameOrPublicKey: String, onError: (() -> ())?) { let maybeSessionId: SessionId? = SessionId(from: onsNameOrPublicKey) if KeyPair.isValidHexEncodedPublicKey(candidate: onsNameOrPublicKey) { @@ -185,7 +185,8 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle title: "ALERT_ERROR_TITLE".localized(), body: .text("DM_ERROR_DIRECT_BLINDED_ID".localized()), cancelTitle: "BUTTON_OK".localized(), - cancelStyle: .alert_text + cancelStyle: .alert_text, + afterClosed: onError ) ) self.present(modal, animated: true) @@ -197,7 +198,8 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle title: "ALERT_ERROR_TITLE".localized(), body: .text("DM_ERROR_INVALID".localized()), cancelTitle: "BUTTON_OK".localized(), - cancelStyle: .alert_text + cancelStyle: .alert_text, + afterClosed: onError ) ) self.present(modal, animated: true) @@ -243,7 +245,8 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle title: "ALERT_ERROR_TITLE".localized(), body: .text(message), cancelTitle: "BUTTON_OK".localized(), - cancelStyle: .alert_text + cancelStyle: .alert_text, + afterClosed: onError ) ) self?.present(modal, animated: true) @@ -663,7 +666,7 @@ private final class EnterPublicKeyVC: UIViewController { @objc fileprivate func startNewDMIfPossible() { let text = publicKeyTextView.text?.trimmingCharacters(in: .whitespaces) ?? "" - NewDMVC.startNewDMIfPossible(with: text) + NewDMVC.startNewDMIfPossible(with: text, onError: nil) } } diff --git a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift index 1a3f6eef5..57ce17769 100644 --- a/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift +++ b/Session/Media Viewing & Editing/GIFs/GiphyAPI.swift @@ -299,7 +299,7 @@ enum GiphyAPI { return HTTPError.generic } .map { data, _ in - Logger.error("search request succeeded") + Logger.debug("search request succeeded") guard let imageInfos = self.parseGiphyImages(responseData: data) else { Logger.error("unable to parse trending images") @@ -347,7 +347,7 @@ enum GiphyAPI { return HTTPError.generic } .tryMap { data, _ -> [GiphyImageInfo] in - Logger.error("search request succeeded") + Logger.debug("search request succeeded") guard let imageInfos = self.parseGiphyImages(responseData: data) else { throw HTTPError.invalidResponse diff --git a/Session/Meta/Session-Info.plist b/Session/Meta/Session-Info.plist index 2ed8191e6..ba0f847ac 100644 --- a/Session/Meta/Session-Info.plist +++ b/Session/Meta/Session-Info.plist @@ -2,13 +2,6 @@ - BuildDetails - - CarthageVersion - 0.36.0 - OSXVersion - 10.15.6 - CFBundleDevelopmentRegion en CFBundleDisplayName diff --git a/Session/Onboarding/LinkDeviceVC.swift b/Session/Onboarding/LinkDeviceVC.swift index 0c4b0af0a..bef5d2460 100644 --- a/Session/Onboarding/LinkDeviceVC.swift +++ b/Session/Onboarding/LinkDeviceVC.swift @@ -134,12 +134,12 @@ final class LinkDeviceVC: BaseVC, UIPageViewControllerDataSource, UIPageViewCont dismiss(animated: true, completion: nil) } - func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String) { + func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?) { let seed = Data(hex: string) - continueWithSeed(seed) + continueWithSeed(seed, onError: onError) } - func continueWithSeed(_ seed: Data) { + func continueWithSeed(_ seed: Data, onError: (() -> ())?) { if (seed.count != 16) { let modal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( @@ -147,9 +147,7 @@ final class LinkDeviceVC: BaseVC, UIPageViewControllerDataSource, UIPageViewCont body: .text("INVALID_RECOVERY_PHRASE_MESSAGE".localized()), cancelTitle: "BUTTON_OK".localized(), cancelStyle: .alert_text, - afterClosed: { [weak self] in - self?.scanQRCodeWrapperVC.startCapture() - } + afterClosed: onError ) ) present(modal, animated: true) @@ -319,7 +317,7 @@ private final class RecoveryPhraseVC: UIViewController { let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic) let seed = Data(hex: hexEncodedSeed) mnemonicTextView.resignFirstResponder() - linkDeviceVC.continueWithSeed(seed) + linkDeviceVC.continueWithSeed(seed, onError: nil) } catch let error { let error = error as? Mnemonic.DecodingError ?? Mnemonic.DecodingError.generic showError(title: error.errorDescription!) diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index 71d39ec73..b9983fa8c 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -144,25 +144,26 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC dismiss(animated: true, completion: nil) } - func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String) { - joinOpenGroup(with: string) + func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?) { + joinOpenGroup(with: string, onError: onError) } - fileprivate func joinOpenGroup(with urlString: String) { + fileprivate func joinOpenGroup(with urlString: String, onError: (() -> ())?) { // A V2 open group URL will look like: + + + + // The host doesn't parse if no explicit scheme is provided guard let (room, server, publicKey) = SessionUtil.parseCommunity(url: urlString) else { showError( title: "invalid_url".localized(), - message: "COMMUNITY_ERROR_INVALID_URL".localized() + message: "COMMUNITY_ERROR_INVALID_URL".localized(), + onError: onError ) return } - joinOpenGroup(roomToken: room, server: server, publicKey: publicKey, shouldOpenCommunity: true) + joinOpenGroup(roomToken: room, server: server, publicKey: publicKey, shouldOpenCommunity: true, onError: onError) } - fileprivate func joinOpenGroup(roomToken: String, server: String, publicKey: String, shouldOpenCommunity: Bool) { + fileprivate func joinOpenGroup(roomToken: String, server: String, publicKey: String, shouldOpenCommunity: Bool, onError: (() -> ())?) { guard !isJoining, let navigationController: UINavigationController = navigationController else { return } isJoining = true @@ -209,7 +210,8 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC self?.dismiss(animated: true) { // Dismiss the loader self?.showError( title: "COMMUNITY_ERROR_GENERIC".localized(), - message: error.localizedDescription + message: error.localizedDescription, + onError: onError ) } @@ -232,13 +234,14 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC // MARK: - Convenience - private func showError(title: String, message: String = "") { + private func showError(title: String, message: String = "", onError: (() -> ())?) { let confirmationModal: ConfirmationModal = ConfirmationModal( info: ConfirmationModal.Info( title: title, body: .text(message), cancelTitle: "BUTTON_OK".localized(), - cancelStyle: .alert_text + cancelStyle: .alert_text, + afterClosed: onError ) ) self.navigationController?.present(confirmationModal, animated: true, completion: nil) @@ -399,13 +402,14 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O roomToken: room.token, server: OpenGroupAPI.defaultServer, publicKey: OpenGroupAPI.defaultServerPublicKey, - shouldOpenCommunity: true + shouldOpenCommunity: true, + onError: nil ) } @objc private func joinOpenGroup() { let url = urlTextView.text?.trimmingCharacters(in: .whitespaces) ?? "" - joinOpenGroupVC?.joinOpenGroup(with: url) + joinOpenGroupVC?.joinOpenGroup(with: url, onError: nil) } // MARK: - Updating diff --git a/Session/Settings/QRCodeVC.swift b/Session/Settings/QRCodeVC.swift index 7f16b4bc7..e5dc85397 100644 --- a/Session/Settings/QRCodeVC.swift +++ b/Session/Settings/QRCodeVC.swift @@ -119,12 +119,12 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl dismiss(animated: true, completion: nil) } - func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String) { + func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?) { let hexEncodedPublicKey = string - startNewPrivateChatIfPossible(with: hexEncodedPublicKey) + startNewPrivateChatIfPossible(with: hexEncodedPublicKey, onError: onError) } - fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String) { + fileprivate func startNewPrivateChatIfPossible(with hexEncodedPublicKey: String, onError: (() -> ())?) { if !KeyPair.isValidHexEncodedPublicKey(candidate: hexEncodedPublicKey) { let modal: ConfirmationModal = ConfirmationModal( targetView: self.view, @@ -132,7 +132,8 @@ final class QRCodeVC : BaseVC, UIPageViewControllerDataSource, UIPageViewControl title: "invalid_session_id".localized(), body: .text("INVALID_SESSION_ID_MESSAGE".localized()), cancelTitle: "BUTTON_OK".localized(), - cancelStyle: .alert_text + cancelStyle: .alert_text, + afterClosed: onError ) ) self.present(modal, animated: true) diff --git a/Session/Shared/QRCodeScanningViewController.swift b/Session/Shared/QRCodeScanningViewController.swift index 011823fa1..b29db5fa5 100644 --- a/Session/Shared/QRCodeScanningViewController.swift +++ b/Session/Shared/QRCodeScanningViewController.swift @@ -2,55 +2,36 @@ import UIKit import AVFoundation -import ZXingObjC import SessionUIKit +import SessionUtilitiesKit protocol QRScannerDelegate: AnyObject { - func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String) + func controller(_ controller: QRCodeScanningViewController, didDetectQRCodeWith string: String, onError: (() -> ())?) } -class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate, ZXCaptureDelegate { +class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate { public weak var scanDelegate: QRScannerDelegate? private let captureQueue: DispatchQueue = DispatchQueue.global(qos: .default) - private var capture: ZXCapture? + private var capture: AVCaptureSession? + private var captureLayer: AVCaptureVideoPreviewLayer? private var captureEnabled: Bool = false // MARK: - Initialization deinit { - self.capture?.layer.removeFromSuperlayer() + self.captureLayer?.removeFromSuperlayer() } // MARK: - Components - private let maskingView: UIView = { - let result: OWSBezierPathView = OWSBezierPathView() - result.configureShapeLayerBlock = { layer, bounds in - // Add a circular mask - let path: UIBezierPath = UIBezierPath(rect: bounds) - let margin: CGFloat = ScaleFromIPhone5To7Plus(24, 48) - let radius: CGFloat = ((min(bounds.size.width, bounds.size.height) * 0.5) - margin) - - // Center the circle's bounding rectangle - let circleRect: CGRect = CGRect( - x: ((bounds.size.width * 0.5) - radius), - y: ((bounds.size.height * 0.5) - radius), - width: (radius * 2), - height: (radius * 2) - ) - let circlePath: UIBezierPath = UIBezierPath.init( - roundedRect: circleRect, - cornerRadius: 16 - ) - path.append(circlePath) - path.usesEvenOddFillRule = true - - layer.path = path.cgPath - layer.fillRule = .evenOdd - layer.themeFillColor = .black - layer.opacity = 0.32 - } + private let maskingView: UIView = UIView() + + private lazy var maskLayer: CAShapeLayer = { + let result: CAShapeLayer = CAShapeLayer() + result.fillRule = .evenOdd + result.themeFillColor = .black + result.opacity = 0.32 return result }() @@ -61,7 +42,8 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj super.loadView() self.view.addSubview(maskingView) - maskingView.pin(to: self.view) + + maskingView.layer.addSublayer(maskLayer) } override func viewWillAppear(_ animated: Bool) { @@ -81,11 +63,28 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() - // Note: When accessing 'capture.layer' if the setup hasn't been completed it - // will result in a layout being triggered which creates an infinite loop, this - // check prevents that case - if let capture: ZXCapture = self.capture { - capture.layer.frame = self.view.bounds + captureLayer?.frame = self.view.bounds + + if maskingView.frame != self.view.bounds { + // Add a circular mask + let path: UIBezierPath = UIBezierPath(rect: self.view.bounds) + let radius: CGFloat = ((min(self.view.bounds.size.width, self.view.bounds.size.height) * 0.5) - Values.largeSpacing) + + // Center the circle's bounding rectangle + let circleRect: CGRect = CGRect( + x: ((self.view.bounds.size.width * 0.5) - radius), + y: ((self.view.bounds.size.height * 0.5) - radius), + width: (radius * 2), + height: (radius * 2) + ) + let clippingPath: UIBezierPath = UIBezierPath.init( + roundedRect: circleRect, + cornerRadius: 16 + ) + path.append(clippingPath) + path.usesEvenOddFillRule = true + + maskLayer.path = path.cgPath } } @@ -101,31 +100,76 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj #else if self.capture == nil { self.captureQueue.async { [weak self] in - let capture: ZXCapture = ZXCapture() - capture.camera = capture.back() - capture.focusMode = .autoFocus - capture.delegate = self - capture.start() + let maybeDevice: AVCaptureDevice? = { + if let result = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) { + return result + } + + return AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) + }() - // Note: When accessing the 'layer' for the first time it will create - // an instance of 'AVCaptureVideoPreviewLayer', this can hang a little - // so we do this on the background thread first - if capture.layer != nil {} + // Set the input device to autoFocus (since we don't have the interaction setup for + // doing it manually) + maybeDevice?.focusMode = .continuousAutoFocus + + // Device input + guard + let device: AVCaptureDevice = maybeDevice, + let input: AVCaptureInput = try? AVCaptureDeviceInput(device: device) + else { + return SNLog("Failed to retrieve the device for enabling the QRCode scanning camera") + } + + // Image output + let output: AVCaptureVideoDataOutput = AVCaptureVideoDataOutput() + output.alwaysDiscardsLateVideoFrames = true + + // Metadata output the session + let metadataOutput: AVCaptureMetadataOutput = AVCaptureMetadataOutput() + metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main) + + let capture: AVCaptureSession = AVCaptureSession() + capture.beginConfiguration() + if capture.canAddInput(input) { capture.addInput(input) } + if capture.canAddOutput(output) { capture.addOutput(output) } + if capture.canAddOutput(metadataOutput) { capture.addOutput(metadataOutput) } + + guard !capture.inputs.isEmpty && capture.outputs.count == 2 else { + return SNLog("Failed to attach the input/output to the capture session") + } + + guard metadataOutput.availableMetadataObjectTypes.contains(.qr) else { + return SNLog("The output is unable to process QR codes") + } + + // Specify that we want to capture QR Codes (Needs to be done after being added + // to the session, 'availableMetadataObjectTypes' is empty beforehand) + metadataOutput.metadataObjectTypes = [.qr] + + capture.commitConfiguration() + + // Create the layer for rendering the camera video + let layer: AVCaptureVideoPreviewLayer = AVCaptureVideoPreviewLayer(session: capture) + layer.videoGravity = AVLayerVideoGravity.resizeAspectFill + + // Start running the capture session + capture.startRunning() DispatchQueue.main.async { - capture.layer.frame = (self?.view.bounds ?? .zero) - self?.view.layer.addSublayer(capture.layer) + layer.frame = (self?.view.bounds ?? .zero) + self?.view.layer.addSublayer(layer) if let maskingView: UIView = self?.maskingView { self?.view.bringSubviewToFront(maskingView) } self?.capture = capture + self?.captureLayer = layer } } } else { - self.capture?.start() + self.capture?.startRunning() } #endif } @@ -133,18 +177,25 @@ class QRCodeScanningViewController: UIViewController, AVCaptureMetadataOutputObj private func stopCapture() { self.captureEnabled = false self.captureQueue.async { [weak self] in - self?.capture?.stop() + self?.capture?.stopRunning() } } - internal func captureResult(_ capture: ZXCapture, result: ZXResult) { - guard self.captureEnabled else { return } + func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) { + guard + self.captureEnabled, + let metadata: AVMetadataObject = metadataObjects.first(where: { ($0 as? AVMetadataMachineReadableCodeObject)?.type == .qr }), + let qrCodeInfo: AVMetadataMachineReadableCodeObject = metadata as? AVMetadataMachineReadableCodeObject, + let qrCode: String = qrCodeInfo.stringValue + else { return } self.stopCapture() // Vibrate AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) - - self.scanDelegate?.controller(self, didDetectQRCodeWith: result.text) + + self.scanDelegate?.controller(self, didDetectQRCodeWith: qrCode) { [weak self] in + self?.startCapture() + } } } diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 5c9771011..b8522beef 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -1,8 +1,9 @@ import Foundation +import GRDB import SessionUtilitiesKit -public enum SNMessagingKit { // Just to make the external API nice - public static func migrations() -> TargetMigrations { +public enum SNMessagingKit: MigratableTarget { // Just to make the external API nice + public static func migrations(_ db: Database) -> TargetMigrations { return TargetMigrations( identifier: .messagingKit, migrations: [ @@ -33,7 +34,7 @@ public enum SNMessagingKit { // Just to make the external API nice // Wait until the feature is turned on before doing the migration that generates // the config dump data // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent - (Features.useSharedUtilForUserConfig() ? + (Features.useSharedUtilForUserConfig(db) ? _014_GenerateInitialUserConfigDumps.self : (nil as Migration.Type?) ), diff --git a/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift index c5bea1d85..c4dd5e383 100644 --- a/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift +++ b/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift @@ -3,7 +3,6 @@ import Foundation import Combine import GRDB -import SessionUtil import SessionSnodeKit import SessionUtilitiesKit diff --git a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift index ad8f54c77..a9c4ffb8d 100644 --- a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift @@ -39,8 +39,7 @@ public enum MessageSendJob: JobExecutor { /// already have attachments in a valid state if details.message is VisibleMessage, - (details.message as? VisibleMessage)?.reaction == nil && - details.isSyncMessage == false + (details.message as? VisibleMessage)?.reaction == nil { guard let jobId: Int64 = job.id, @@ -51,122 +50,111 @@ public enum MessageSendJob: JobExecutor { return } - // If the original interaction no longer exists then don't bother sending the message (ie. the - // message was deleted before it even got sent) - guard Storage.shared.read({ db in try Interaction.exists(db, id: interactionId) }) == true else { - SNLog("[MessageSendJob] Failing due to missing interaction") - failure(job, StorageError.objectNotFound, true) - return - } - - // Check if there are any attachments associated to this message, and if so - // upload them now - // - // Note: Normal attachments should be sent in a non-durable way but any - // attachments for LinkPreviews and Quotes will be processed through this mechanism - let attachmentState: (shouldFail: Bool, shouldDefer: Bool, fileIds: [String])? = Storage.shared.write { db in - let allAttachmentStateInfo: [Attachment.StateInfo] = try Attachment - .stateInfo(interactionId: interactionId) - .fetchAll(db) - let maybeFileIds: [String?] = allAttachmentStateInfo - .sorted { lhs, rhs in lhs.albumIndex < rhs.albumIndex } - .map { Attachment.fileId(for: $0.downloadUrl) } - let fileIds: [String] = maybeFileIds.compactMap { $0 } - - // If there were failed attachments then this job should fail (can't send a - // message which has associated attachments if the attachments fail to upload) - guard !allAttachmentStateInfo.contains(where: { $0.state == .failedDownload }) else { - return (true, false, fileIds) - } - - // Create jobs for any pending (or failed) attachment jobs and insert them into the - // queue before the current job (this will mean the current job will re-run - // after these inserted jobs complete) - // - // Note: If there are any 'downloaded' attachments then they also need to be - // uploaded (as a 'downloaded' attachment will be on the current users device - // but not on the message recipients device - both LinkPreview and Quote can - // have this case) - try allAttachmentStateInfo - .filter { attachment -> Bool in - // Non-media quotes won't have thumbnails so so don't try to upload them - guard attachment.downloadUrl != Attachment.nonMediaQuoteFileId else { return false } - - switch attachment.state { - case .uploading, .pendingDownload, .downloading, .failedUpload, .downloaded: - return true - - default: return false + // Retrieve the current attachment state + typealias AttachmentState = (error: Error?, pendingUploadAttachmentIds: [String], preparedFileIds: [String]) + + let attachmentState: AttachmentState = Storage.shared + .read { db in + // If the original interaction no longer exists then don't bother sending the message (ie. the + // message was deleted before it even got sent) + guard try Interaction.exists(db, id: interactionId) else { + SNLog("[MessageSendJob] Failing due to missing interaction") + return (StorageError.objectNotFound, [], []) + } + + // Get the current state of the attachments + let allAttachmentStateInfo: [Attachment.StateInfo] = try Attachment + .stateInfo(interactionId: interactionId) + .fetchAll(db) + let maybeFileIds: [String?] = allAttachmentStateInfo + .sorted { lhs, rhs in lhs.albumIndex < rhs.albumIndex } + .map { Attachment.fileId(for: $0.downloadUrl) } + let fileIds: [String] = maybeFileIds.compactMap { $0 } + + // If there were failed attachments then this job should fail (can't send a + // message which has associated attachments if the attachments fail to upload) + guard !allAttachmentStateInfo.contains(where: { $0.state == .failedDownload }) else { + SNLog("[MessageSendJob] Failing due to failed attachment upload") + return (AttachmentError.notUploaded, [], fileIds) + } + + /// Find all attachmentIds for attachments which need to be uploaded + /// + /// **Note:** If there are any 'downloaded' attachments then they also need to be uploaded (as a + /// 'downloaded' attachment will be on the current users device but not on the message recipients + /// device - both `LinkPreview` and `Quote` can have this case) + let pendingUploadAttachmentIds: [String] = allAttachmentStateInfo + .filter { attachment -> Bool in + // Non-media quotes won't have thumbnails so so don't try to upload them + guard attachment.downloadUrl != Attachment.nonMediaQuoteFileId else { return false } + + switch attachment.state { + case .uploading, .pendingDownload, .downloading, .failedUpload, .downloaded: + return true + + default: return false + } } - } - .filter { stateInfo in - // Don't add a new job if there is one already in the queue - !JobRunner.hasPendingOrRunningJob( - with: .attachmentUpload, - details: AttachmentUploadJob.Details( - messageSendJobId: jobId, - attachmentId: stateInfo.attachmentId - ) - ) - } - .compactMap { stateInfo -> (jobId: Int64, job: Job)? in - JobRunner - .insert( - db, - job: Job( - variant: .attachmentUpload, - behaviour: .runOnce, - threadId: job.threadId, - interactionId: interactionId, - details: AttachmentUploadJob.Details( - messageSendJobId: jobId, - attachmentId: stateInfo.attachmentId - ) - ), - before: job - ) - } - .forEach { otherJobId, _ in - // Create the dependency between the jobs - try JobDependencies( - jobId: jobId, - dependantId: otherJobId - ) - .insert(db) - } - - // If there were pending or uploading attachments then stop here (we want to - // upload them first and then re-run this send job - the 'JobRunner.insert' - // method will take care of this) - let isMissingFileIds: Bool = (maybeFileIds.count != fileIds.count) - let hasPendingUploads: Bool = allAttachmentStateInfo.contains(where: { $0.state != .uploaded }) - - return ( - (isMissingFileIds && !hasPendingUploads), - hasPendingUploads, - fileIds - ) - } - - // Don't send messages with failed attachment uploads - // - // Note: If we have gotten to this point then any dependant attachment upload - // jobs will have permanently failed so this message send should also do so - guard attachmentState?.shouldFail == false else { - SNLog("[MessageSendJob] Failing due to failed attachment upload") - failure(job, AttachmentError.notUploaded, true) - return + .map { $0.attachmentId } + + return (nil, pendingUploadAttachmentIds, fileIds) + } + .defaulting(to: (MessageSenderError.invalidMessage, [], [])) + + /// If we got an error when trying to retrieve the attachment state then this job is actually invalid so it + /// should permanently fail + guard attachmentState.error == nil else { + return failure(job, (attachmentState.error ?? MessageSenderError.invalidMessage), true) } - // Defer the job if we found incomplete uploads - guard attachmentState?.shouldDefer == false else { - SNLog("[MessageSendJob] Deferring pending attachment uploads") - deferred(job) - return + /// If we have any pending (or failed) attachment uploads then we should create jobs for them and insert them into the + /// queue before the current job and defer it (this will mean the current job will re-run after these inserted jobs complete) + guard attachmentState.pendingUploadAttachmentIds.isEmpty else { + Storage.shared.write { db in + try attachmentState.pendingUploadAttachmentIds + .filter { attachmentId in + // Don't add a new job if there is one already in the queue + !JobRunner.hasPendingOrRunningJob( + with: .attachmentUpload, + details: AttachmentUploadJob.Details( + messageSendJobId: jobId, + attachmentId: attachmentId + ) + ) + } + .compactMap { attachmentId -> (jobId: Int64, job: Job)? in + JobRunner + .insert( + db, + job: Job( + variant: .attachmentUpload, + behaviour: .runOnce, + threadId: job.threadId, + interactionId: interactionId, + details: AttachmentUploadJob.Details( + messageSendJobId: jobId, + attachmentId: attachmentId + ) + ), + before: job + ) + } + .forEach { otherJobId, _ in + // Create the dependency between the jobs + try JobDependencies( + jobId: jobId, + dependantId: otherJobId + ) + .insert(db) + } + } + + SNLog("[MessageSendJob] Deferring due to pending attachment uploads") + return deferred(job) } - + // Store the fileIds so they can be sent with the open group message content - messageFileIds = (attachmentState?.fileIds ?? []) + messageFileIds = attachmentState.preparedFileIds } // Store the sentTimestamp from the message in case it fails due to a clockOutOfSync error diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index b129902e5..7de0c095a 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -610,6 +610,21 @@ public final class MessageSender { ) guard expectedAttachmentUploadCount == preparedSendData.totalAttachmentsUploaded else { + // Make sure to actually handle this as a failure (if we don't then the message + // won't go into an error state correctly) + if let message: Message = preparedSendData.message { + dependencies.storage.read { db in + MessageSender.handleFailedMessageSend( + db, + message: message, + with: .attachmentsNotUploaded, + interactionId: preparedSendData.interactionId, + isSyncMessage: (preparedSendData.isSyncMessage == true), + using: dependencies + ) + } + } + return Fail(error: MessageSenderError.attachmentsNotUploaded) .eraseToAnyPublisher() } @@ -1007,7 +1022,6 @@ public final class MessageSender { isSyncMessage: Bool = false, using dependencies: SMKDependencies = SMKDependencies() ) -> Error { - // TODO: Revert the local database change // If the message was a reaction then we don't want to do anything to the original // interaciton (which the 'interactionId' is pointing to guard (message as? VisibleMessage)?.reaction == nil else { return error } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift index 02b793160..3936baf4f 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/CurrentUserPoller.swift @@ -14,7 +14,12 @@ public final class CurrentUserPoller: Poller { // MARK: - Settings - override var namespaces: [SnodeAPI.Namespace] { CurrentUserPoller.namespaces } + override var namespaces: [SnodeAPI.Namespace] { + // FIXME: Remove this once `useSharedUtilForUserConfig` is permanent + guard SessionUtil.userConfigsEnabled else { return [.default] } + + return CurrentUserPoller.namespaces + } /// After polling a given snode this many times we always switch to a new one. /// diff --git a/SessionMessagingKit/Sending & Receiving/Quotes/QuotedReplyModel.swift b/SessionMessagingKit/Sending & Receiving/Quotes/QuotedReplyModel.swift index 562621a36..94a6c2ee4 100644 --- a/SessionMessagingKit/Sending & Receiving/Quotes/QuotedReplyModel.swift +++ b/SessionMessagingKit/Sending & Receiving/Quotes/QuotedReplyModel.swift @@ -71,16 +71,3 @@ public struct QuotedReplyModel { ) } } - -// MARK: - Convenience - -public extension QuotedReplyModel { - func generateAttachmentThumbnailIfNeeded(_ db: Database) throws -> String? { - guard let sourceAttachment: Attachment = self.attachment else { return nil } - - return try sourceAttachment - .cloneAsQuoteThumbnail()? - .inserted(db) - .id - } -} diff --git a/SessionMessagingKit/SessionUtil/SessionUtil.swift b/SessionMessagingKit/SessionUtil/SessionUtil.swift index 62b77c9fe..d933238a5 100644 --- a/SessionMessagingKit/SessionUtil/SessionUtil.swift +++ b/SessionMessagingKit/SessionUtil/SessionUtil.swift @@ -10,9 +10,7 @@ import SessionUtilitiesKit public extension Features { static func useSharedUtilForUserConfig(_ db: Database? = nil) -> Bool { - return true - // TODO: Need to set this timestamp to the correct date (currently start of 2030) -// guard Date().timeIntervalSince1970 < 1893456000 else { return true } + guard Date().timeIntervalSince1970 < 1690761600 else { return true } guard !SessionUtil.hasCheckedMigrationsCompleted.wrappedValue else { return SessionUtil.userConfigsEnabledIgnoringFeatureFlag } diff --git a/SessionMessagingKit/Shared Models/MessageViewModel.swift b/SessionMessagingKit/Shared Models/MessageViewModel.swift index 67d719d74..57fd9acc8 100644 --- a/SessionMessagingKit/Shared Models/MessageViewModel.swift +++ b/SessionMessagingKit/Shared Models/MessageViewModel.swift @@ -527,6 +527,7 @@ public extension MessageViewModel { public extension MessageViewModel { static let genericId: Int64 = -1 static let typingIndicatorId: Int64 = -2 + static let optimisticUpdateId: Int64 = -3 /// This init method is only used for system-created cells or empty states init( @@ -634,8 +635,8 @@ public extension MessageViewModel { // Interaction Info - self.rowId = -1 - self.id = -1 + self.rowId = MessageViewModel.optimisticUpdateId + self.id = MessageViewModel.optimisticUpdateId self.openGroupServerMessageId = nil self.variant = .standardOutgoing self.timestampMs = timestampMs diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index c74bf5c2c..d87bfdca3 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -37,9 +37,9 @@ class OpenGroupAPISpec: QuickSpec { beforeEach { mockStorage = Storage( customWriter: try! DatabaseQueue(), - customMigrations: [ - SNUtilitiesKit.migrations(), - SNMessagingKit.migrations() + customMigrationTargets: [ + SNUtilitiesKit.self, + SNMessagingKit.self ] ) mockSodium = MockSodium() diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift index eb93e936c..5386c3207 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupManagerSpec.swift @@ -103,9 +103,9 @@ class OpenGroupManagerSpec: QuickSpec { mockGeneralCache = MockGeneralCache() mockStorage = Storage( customWriter: try! DatabaseQueue(), - customMigrations: [ - SNUtilitiesKit.migrations(), - SNMessagingKit.migrations() + customMigrationTargets: [ + SNUtilitiesKit.self, + SNMessagingKit.self ] ) mockSodium = MockSodium() @@ -3581,7 +3581,7 @@ class OpenGroupManagerSpec: QuickSpec { forKey: SNUserDefaults.Date.lastOpenGroupImageUpdate.rawValue ) }, - timeout: .milliseconds(50) + timeout: .milliseconds(100) ) expect( mockStorage.read { db -> Data? in diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverDecryptionSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverDecryptionSpec.swift index b9066ba26..7f9a45a13 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageReceiverDecryptionSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageReceiverDecryptionSpec.swift @@ -27,9 +27,9 @@ class MessageReceiverDecryptionSpec: QuickSpec { beforeEach { mockStorage = Storage( customWriter: try! DatabaseQueue(), - customMigrations: [ - SNUtilitiesKit.migrations(), - SNMessagingKit.migrations() + customMigrationTargets: [ + SNUtilitiesKit.self, + SNMessagingKit.self ] ) mockSodium = MockSodium() diff --git a/SessionMessagingKitTests/Sending & Receiving/MessageSenderEncryptionSpec.swift b/SessionMessagingKitTests/Sending & Receiving/MessageSenderEncryptionSpec.swift index 6334229a1..f937b3744 100644 --- a/SessionMessagingKitTests/Sending & Receiving/MessageSenderEncryptionSpec.swift +++ b/SessionMessagingKitTests/Sending & Receiving/MessageSenderEncryptionSpec.swift @@ -24,9 +24,9 @@ class MessageSenderEncryptionSpec: QuickSpec { beforeEach { mockStorage = Storage( customWriter: try! DatabaseQueue(), - customMigrations: [ - SNUtilitiesKit.migrations(), - SNMessagingKit.migrations() + customMigrationTargets: [ + SNUtilitiesKit.self, + SNMessagingKit.self ] ) mockBox = MockBox() diff --git a/SessionSnodeKit/Configuration.swift b/SessionSnodeKit/Configuration.swift index dcf685839..7f8a597d4 100644 --- a/SessionSnodeKit/Configuration.swift +++ b/SessionSnodeKit/Configuration.swift @@ -1,10 +1,11 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import GRDB import SessionUtilitiesKit -public enum SNSnodeKit { // Just to make the external API nice - public static func migrations() -> TargetMigrations { +public enum SNSnodeKit: MigratableTarget { // Just to make the external API nice + public static func migrations(_ db: Database) -> TargetMigrations { return TargetMigrations( identifier: .snodeKit, migrations: [ diff --git a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift index 8b444d17e..e60eefdfe 100644 --- a/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift @@ -26,11 +26,11 @@ class ThreadDisappearingMessagesSettingsViewModelSpec: QuickSpec { beforeEach { mockStorage = Storage( customWriter: try! DatabaseQueue(), - customMigrations: [ - SNUtilitiesKit.migrations(), - SNSnodeKit.migrations(), - SNMessagingKit.migrations(), - SNUIKit.migrations() + customMigrationTargets: [ + SNUtilitiesKit.self, + SNSnodeKit.self, + SNMessagingKit.self, + SNUIKit.self ] ) dependencies = Dependencies( diff --git a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift index eaf4b915b..60ed929db 100644 --- a/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift +++ b/SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift @@ -28,11 +28,11 @@ class ThreadSettingsViewModelSpec: QuickSpec { beforeEach { mockStorage = SynchronousStorage( customWriter: try! DatabaseQueue(), - customMigrations: [ - SNUtilitiesKit.migrations(), - SNSnodeKit.migrations(), - SNMessagingKit.migrations(), - SNUIKit.migrations() + customMigrationTargets: [ + SNUtilitiesKit.self, + SNSnodeKit.self, + SNMessagingKit.self, + SNUIKit.self ] ) mockGeneralCache = MockGeneralCache() diff --git a/SessionTests/Settings/NotificationContentViewModelSpec.swift b/SessionTests/Settings/NotificationContentViewModelSpec.swift index 81a838f15..e6d1e5999 100644 --- a/SessionTests/Settings/NotificationContentViewModelSpec.swift +++ b/SessionTests/Settings/NotificationContentViewModelSpec.swift @@ -25,11 +25,11 @@ class NotificationContentViewModelSpec: QuickSpec { beforeEach { mockStorage = Storage( customWriter: try! DatabaseQueue(), - customMigrations: [ - SNUtilitiesKit.migrations(), - SNSnodeKit.migrations(), - SNMessagingKit.migrations(), - SNUIKit.migrations() + customMigrationTargets: [ + SNUtilitiesKit.self, + SNSnodeKit.self, + SNMessagingKit.self, + SNUIKit.self ] ) viewModel = NotificationContentViewModel(storage: mockStorage, scheduling: .immediate) diff --git a/SessionUIKit/Configuration.swift b/SessionUIKit/Configuration.swift index d305968e9..798ba98eb 100644 --- a/SessionUIKit/Configuration.swift +++ b/SessionUIKit/Configuration.swift @@ -1,10 +1,11 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import GRDB import SessionUtilitiesKit -public enum SNUIKit { - public static func migrations() -> TargetMigrations { +public enum SNUIKit: MigratableTarget { + public static func migrations(_ db: Database) -> TargetMigrations { return TargetMigrations( identifier: .uiKit, migrations: [ diff --git a/SessionUtilitiesKit/Configuration.swift b/SessionUtilitiesKit/Configuration.swift index 616e27ed3..df5b5b366 100644 --- a/SessionUtilitiesKit/Configuration.swift +++ b/SessionUtilitiesKit/Configuration.swift @@ -1,9 +1,10 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import GRDB -public enum SNUtilitiesKit { // Just to make the external API nice - public static func migrations() -> TargetMigrations { +public enum SNUtilitiesKit: MigratableTarget { // Just to make the external API nice + public static func migrations(_ db: Database) -> TargetMigrations { return TargetMigrations( identifier: .utilitiesKit, migrations: [ diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 12f0bd57d..27eb4877e 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -47,14 +47,14 @@ open class Storage { public init( customWriter: DatabaseWriter? = nil, - customMigrations: [TargetMigrations]? = nil + customMigrationTargets: [MigratableTarget.Type]? = nil ) { - configureDatabase(customWriter: customWriter, customMigrations: customMigrations) + configureDatabase(customWriter: customWriter, customMigrationTargets: customMigrationTargets) } private func configureDatabase( customWriter: DatabaseWriter? = nil, - customMigrations: [TargetMigrations]? = nil + customMigrationTargets: [MigratableTarget.Type]? = nil ) { // Create the database directory if needed and ensure it's protection level is set before attempting to // create the database KeySpec or the database itself @@ -66,7 +66,12 @@ open class Storage { dbWriter = customWriter isValid = true Storage.internalHasCreatedValidInstance.mutate { $0 = true } - perform(migrations: (customMigrations ?? []), async: false, onProgressUpdate: nil, onComplete: { _, _ in }) + perform( + migrationTargets: (customMigrationTargets ?? []), + async: false, + onProgressUpdate: nil, + onComplete: { _, _ in } + ) return } @@ -128,7 +133,7 @@ open class Storage { } public func perform( - migrations: [TargetMigrations], + migrationTargets: [MigratableTarget.Type], async: Bool = true, onProgressUpdate: ((CGFloat, TimeInterval) -> ())?, onComplete: @escaping (Swift.Result, Bool) -> () @@ -141,18 +146,28 @@ open class Storage { } typealias MigrationInfo = (identifier: TargetMigrations.Identifier, migrations: TargetMigrations.MigrationSet) - let sortedMigrationInfo: [MigrationInfo] = migrations - .sorted() - .reduce(into: [[MigrationInfo]]()) { result, next in - next.migrations.enumerated().forEach { index, migrationSet in - if result.count <= index { - result.append([]) - } + let maybeSortedMigrationInfo: [MigrationInfo]? = try? dbWriter + .read { db -> [MigrationInfo] in + migrationTargets + .map { target -> TargetMigrations in target.migrations(db) } + .sorted() + .reduce(into: [[MigrationInfo]]()) { result, next in + next.migrations.enumerated().forEach { index, migrationSet in + if result.count <= index { + result.append([]) + } - result[index] = (result[index] + [(next.identifier, migrationSet)]) - } + result[index] = (result[index] + [(next.identifier, migrationSet)]) + } + } + .reduce(into: []) { result, next in result.append(contentsOf: next) } } - .reduce(into: []) { result, next in result.append(contentsOf: next) } + + guard let sortedMigrationInfo: [MigrationInfo] = maybeSortedMigrationInfo else { + SNLog("[Database Error] Statup failed with error: Unable to prepare migrations") + onComplete(.failure(StorageError.startupFailed), false) + return + } // Setup and run any required migrations migrator = { [weak self] in diff --git a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift index ad305ab73..f9627a7e8 100644 --- a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift +++ b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift @@ -1,6 +1,11 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import GRDB + +public protocol MigratableTarget { + static func migrations(_ db: Database) -> TargetMigrations +} public struct TargetMigrations: Comparable { /// This identifier is used to determine the order each set of migrations should run in. diff --git a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift index 7f5ed9da6..7eeb39293 100644 --- a/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift +++ b/SessionUtilitiesKitTests/Database/Models/IdentitySpec.swift @@ -18,8 +18,8 @@ class IdentitySpec: QuickSpec { beforeEach { mockStorage = Storage( customWriter: try! DatabaseQueue(), - customMigrations: [ - SNUtilitiesKit.migrations() + customMigrationTargets: [ + SNUtilitiesKit.self ] ) } diff --git a/SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift b/SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift index 2c7b09ce4..e17536fe7 100644 --- a/SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift +++ b/SessionUtilitiesKitTests/Database/Utilities/PersistableRecordUtilitiesSpec.swift @@ -84,6 +84,17 @@ class PersistableRecordUtilitiesSpec: QuickSpec { } } + private struct TestTarget: MigratableTarget { + static func migrations(_ db: Database) -> TargetMigrations { + return TargetMigrations( + identifier: .test, + migrations: (0..<100) + .map { _ in [] } + .appending([TestInsertTestTypeMigration.self]) + ) + } + } + // MARK: - Spec override func spec() { @@ -96,13 +107,8 @@ class PersistableRecordUtilitiesSpec: QuickSpec { PersistableRecordUtilitiesSpec.customWriter = customWriter mockStorage = Storage( customWriter: customWriter, - customMigrations: [ - TargetMigrations( - identifier: .test, - migrations: (0..<100) - .map { _ in [] } - .appending([TestInsertTestTypeMigration.self]) - ) + customMigrationTargets: [ + TestTarget.self ] ) } diff --git a/SignalUtilitiesKit/Utilities/AppSetup.swift b/SignalUtilitiesKit/Utilities/AppSetup.swift index efc6d1c25..58f0c6856 100644 --- a/SignalUtilitiesKit/Utilities/AppSetup.swift +++ b/SignalUtilitiesKit/Utilities/AppSetup.swift @@ -66,11 +66,11 @@ public enum AppSetup { var backgroundTask: OWSBackgroundTask? = (backgroundTask ?? OWSBackgroundTask(labelStr: #function)) Storage.shared.perform( - migrations: [ - SNUtilitiesKit.migrations(), - SNSnodeKit.migrations(), - SNMessagingKit.migrations(), - SNUIKit.migrations() + migrationTargets: [ + SNUtilitiesKit.self, + SNSnodeKit.self, + SNMessagingKit.self, + SNUIKit.self ], onProgressUpdate: migrationProgressChanged, onComplete: { result, needsConfigSync in