diff --git a/.github/actions/asana-add-comment/templates/hotfix-branch-ready.yml b/.github/actions/asana-add-comment/templates/hotfix-branch-ready.yml
new file mode 100644
index 0000000000..635d4b42b5
--- /dev/null
+++ b/.github/actions/asana-add-comment/templates/hotfix-branch-ready.yml
@@ -0,0 +1,13 @@
+data:
+ # yq -o=j | sed -E 's/\\n( *)([^\\n])/\2/g'
+ html_text: |
+
+ Hotfix branch ${BRANCH} ready βοΈ
+
+ π± ${BRANCH}
branch has been created off ${RELEASE_TAG}
tag.
+ Point any pull requests with changes required for the hotfix release to that branch.
+
+
+
+ π Workflow URL: ${WORKFLOW_URL} .
+
diff --git a/.github/actions/asana-add-comment/templates/internal-release-ready-tag-failed copy.yml b/.github/actions/asana-add-comment/templates/internal-release-ready-tag-failed copy.yml
deleted file mode 100644
index f98641fa62..0000000000
--- a/.github/actions/asana-add-comment/templates/internal-release-ready-tag-failed copy.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-data:
- html_text: |
-
- [ACTION NEEDED] Internal release build ${TAG} ready
-
- π₯ DMG is available from ${DMG_URL} .
- βοΈ Tagging repository failed.
- β οΈ GitHub release creation was skipped.
- β οΈ Merging ${BRANCH}
to ${BASE_BRANCH}
was skipped.
-
-
- , please proceed with manual tagging and merging according to instructions .
-
-
- π Workflow URL: ${WORKFLOW_URL} .
-
diff --git a/.github/actions/asana-add-comment/templates/validate-check-for-updates-public.yml b/.github/actions/asana-add-comment/templates/validate-check-for-updates-public.yml
index d27c1cae3c..285e94c2a1 100644
--- a/.github/actions/asana-add-comment/templates/validate-check-for-updates-public.yml
+++ b/.github/actions/asana-add-comment/templates/validate-check-for-updates-public.yml
@@ -4,7 +4,7 @@ data:
Build ${TAG} is available publicly through Sparkle π
π New appcast file has been generated and uploaded to S3, together with binary delta files.
- π , please proceed by following instructions in which concludes the release process.
+ π , please proceed by following instructions in and which concludes the release process.
π Workflow URL: ${WORKFLOW_URL} .
diff --git a/.github/actions/asana-create-action-item/action.yml b/.github/actions/asana-create-action-item/action.yml
index 99b0cc8f1d..3a3092d1bc 100644
--- a/.github/actions/asana-create-action-item/action.yml
+++ b/.github/actions/asana-create-action-item/action.yml
@@ -13,8 +13,12 @@ inputs:
description: "Task name"
required: false
type: string
- contents:
- description: "Task contents"
+ notes:
+ description: "Task notes"
+ required: false
+ type: string
+ html-notes:
+ description: "Task HTML notes"
required: false
type: string
template-name:
@@ -55,16 +59,28 @@ runs:
payload="$(envsubst < $TEMPLATE_PATH | yq -o=j | sed -E 's/\\n( *)([^\\n])/\2/g' | jq -c)"
echo "payload-base64=$(base64 <<< $payload)" >> $GITHUB_OUTPUT
- - id: process-contents-payload
- if: ${{ inputs.contents }}
+ - id: process-notes-payload
+ if: ${{ inputs.notes }}
+ shell: bash
+ env:
+ ASSIGNEE_ID: ${{ steps.get-automation-subtask.outputs.assignee-id }}
+ NOTES: ${{ inputs.notes }}
+ TASK_NAME: ${{ inputs.task-name }}
+ WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ run: |
+ payload="{ \"data\": { \"name\": \"${TASK_NAME}\", \"notes\": \"${NOTES}\n\nπ Workflow URL: ${WORKFLOW_URL}\", \"assignee\": \"${ASSIGNEE_ID}\" } }"
+ echo "payload-base64=$(base64 <<< $payload)" >> $GITHUB_OUTPUT
+
+ - id: process-html-notes-payload
+ if: ${{ inputs.html-notes }}
shell: bash
env:
ASSIGNEE_ID: ${{ steps.get-automation-subtask.outputs.assignee-id }}
- CONTENTS: ${{ inputs.contents }}
+ HTML_NOTES: ${{ inputs.html-notes }}
TASK_NAME: ${{ inputs.task-name }}
WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
- payload="{ \"data\": { \"name\": \"${TASK_NAME}\", \"notes\": \"${CONTENTS}\n\nWorkflow URL: ${WORKFLOW_URL}\", \"assignee\": \"${ASSIGNEE_ID}\" } }"
+ payload="{ \"data\": { \"name\": \"${TASK_NAME}\", \"html_notes\": \"${HTML_NOTES}\", \"assignee\": \"${ASSIGNEE_ID}\" } }"
echo "payload-base64=$(base64 <<< $payload)" >> $GITHUB_OUTPUT
- id: create-task
@@ -73,7 +89,7 @@ runs:
ASANA_ACCESS_TOKEN: ${{ inputs.access-token }}
ASSIGNEE_ID: ${{ steps.get-automation-subtask.outputs.assignee-id }}
TASK_ID: ${{ steps.get-automation-subtask.outputs.automation-task-id }}
- PAYLOAD_BASE64: ${{ steps.process-template-payload.outputs.payload-base64 || steps.process-comment-payload.outputs.payload-base64 }}
+ PAYLOAD_BASE64: ${{ steps.process-template-payload.outputs.payload-base64 || steps.process-notes-payload.outputs.payload-base64 || steps.process-html-notes-payload.outputs.payload-base64 }}
run: |
# Create a subtask and retrieve its ID from the response (.data.gid) to return as an output
new_task_id=$(set -o pipefail && curl -fLSs "https://app.asana.com/api/1.0/tasks/${TASK_ID}/subtasks?opt_fields=gid" \
diff --git a/.github/actions/asana-create-action-item/templates/internal-release-tag-failed.yml b/.github/actions/asana-create-action-item/templates/internal-release-tag-failed.yml
index 9d617e0eaf..edb1cc2cef 100644
--- a/.github/actions/asana-create-action-item/templates/internal-release-tag-failed.yml
+++ b/.github/actions/asana-create-action-item/templates/internal-release-tag-failed.yml
@@ -19,7 +19,12 @@ data:
git pull origin ${BASE_BRANCH}
pull the latest code
git merge ${BRANCH}
- resolve conflicts as needed
+ Resolve conflicts as needed
+ When merging a hotfix branch into an internal release branch, you will get conflicts in version and build number xcconfig files:
+
+ In the version file: accept the internal version number (higher).
+ In the build number file: accept the hotfix build number (higher). This step is very important in order to calculate the build number of the next internal release correctly.
+
git push origin ${BASE_BRANCH}
push merged branch
diff --git a/.github/actions/asana-create-action-item/templates/merge-failed.yml b/.github/actions/asana-create-action-item/templates/merge-failed.yml
index f4d997e98b..db6e4c8aa7 100644
--- a/.github/actions/asana-create-action-item/templates/merge-failed.yml
+++ b/.github/actions/asana-create-action-item/templates/merge-failed.yml
@@ -16,7 +16,12 @@ data:
git pull origin ${BASE_BRANCH}
pull the latest code
git merge ${BRANCH}
- resolve conflicts as needed
+ Resolve conflicts as needed
+ When merging a hotfix branch into an internal release branch, you will get conflicts in version and build number xcconfig files:
+
+ In the version file: accept the internal version number (higher).
+ In the build number file: accept the hotfix build number (higher). This step is very important in order to calculate the build number of the next internal release correctly.
+
git push origin ${BASE_BRANCH}
push merged branch
diff --git a/.github/actions/asana-create-action-item/templates/update-asana-for-public-release.yml b/.github/actions/asana-create-action-item/templates/update-asana-for-public-release.yml
new file mode 100644
index 0000000000..17a7c696c5
--- /dev/null
+++ b/.github/actions/asana-create-action-item/templates/update-asana-for-public-release.yml
@@ -0,0 +1,19 @@
+data:
+ name: Move release task and included items to "Done" section in macOS App Board and close them if possible
+ assignee: "${ASSIGNEE_ID}"
+ html_notes: |
+
+ Automation failed to update Asana for the public release. Please follow the steps below.
+
+ Open and select the List view
+ Scroll to the "Validation" section.
+ Select all the tasks in that section.
+ Drag and drop all the selected tasks to the "Done" section
+ Close all tasks that are not incidents and don't belong to project, including the release task itself.
+
+
+ Complete this task when ready.
+
+
+ π Workflow URL: ${WORKFLOW_URL} .
+
diff --git a/.github/workflows/build_hotfix_release.yml b/.github/workflows/build_hotfix_release.yml
new file mode 100644
index 0000000000..0faa9d7517
--- /dev/null
+++ b/.github/workflows/build_hotfix_release.yml
@@ -0,0 +1,124 @@
+name: Build Hotfix Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ asana-task-url:
+ description: "Asana release task URL"
+ required: true
+ type: string
+ base-branch:
+ description: "Base branch (defaults to main, only override for testing)"
+ required: false
+ type: string
+ current-internal-release-branch:
+ description: "Current internal release branch (to merge hotfix branch to - hotfix branch is merged to main if this is not provided)"
+ required: false
+ type: string
+
+jobs:
+
+ assert_release_branch:
+
+ name: Assert Hotfix Branch
+
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+
+ - name: Assert hotfix release branch
+ run: |
+ case "${{ github.ref }}" in
+ refs/heads/hotfix/*) ;;
+ *) echo "π Not a hotfix release branch"; exit 1 ;;
+ esac
+
+ run_tests:
+
+ name: Run Tests
+
+ needs: assert_release_branch
+ uses: ./.github/workflows/pr.yml
+ secrets:
+ ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
+
+ update_asana:
+
+ name: Update Asana tasks
+
+ needs: run_tests
+ runs-on: macos-13-xlarge
+ timeout-minutes: 10
+
+ steps:
+
+ - name: Check out the code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Fetch all history and tags in order to extract Asana task URLs from git log
+ ref: ${{ github.ref_name }}
+ submodules: recursive
+
+ - name: Extract Asana Task ID
+ id: task-id
+ uses: ./.github/actions/asana-extract-task-id
+ with:
+ task-url: ${{ github.event.inputs.asana-task-url }}
+
+ - name: Update Asana tasks for the release
+ env:
+ ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
+ GH_TOKEN: ${{ github.token }}
+ BRANCH: ${{ github.ref_name }}
+ run: |
+ version="$(cut -d '/' -f 2 <<< "$BRANCH")"
+ # 'internal', because we start with making a build that still needs to be tested before being published
+ # and we want Asana tasks to be moved to "Validation" and not already to "Done"
+ ./scripts/update_asana_for_release.sh internal ${{ steps.task-id.outputs.task-id }} ${{ vars.MACOS_APP_BOARD_VALIDATION_SECTION_ID }} "${version}"
+
+ prepare_release:
+ name: Prepare Release
+ needs: run_tests
+ uses: ./.github/workflows/release.yml
+ with:
+ asana-task-url: ${{ github.event.inputs.asana-task-url }}
+ secrets:
+ BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
+ P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.REVIEW_PROVISION_PROFILE_BASE64 }}
+ RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.RELEASE_PROVISION_PROFILE_BASE64 }}
+ DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64 }}
+ DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64 }}
+ NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64_V2: ${{ secrets.NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64_V2 }}
+ NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64_V2: ${{ secrets.NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64_V2 }}
+ NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64_V2: ${{ secrets.NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64_V2 }}
+ NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64_V2: ${{ secrets.NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64_V2 }}
+ NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64 }}
+ NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64 }}
+ APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
+ APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
+ APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }}
+ ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
+ MM_HANDLES_BASE64: ${{ secrets.MM_HANDLES_BASE64 }}
+ MM_WEBHOOK_URL: ${{ secrets.MM_WEBHOOK_URL }}
+ AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
+ AWS_ACCESS_KEY_ID_RELEASE_S3: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }}
+ AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
+ AWS_SECRET_ACCESS_KEY_RELEASE_S3: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }}
+ MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
+ SSH_PRIVATE_KEY_FASTLANE_MATCH: ${{ secrets.SSH_PRIVATE_KEY_FASTLANE_MATCH }}
+
+ tag_and_merge:
+ name: Tag and Merge Branch
+ needs: [ prepare_release, update_asana ]
+ uses: ./.github/workflows/tag_release.yml
+ with:
+ asana-task-url: ${{ github.event.inputs.asana-task-url }}
+ branch: ${{ github.ref_name }}
+ base-branch: ${{ github.event.inputs.current-internal-release-branch || 'main' }}
+ prerelease: true # Pre-release for now, and the actual release will be done as part of publish_dmg_release that's called later
+ secrets:
+ ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
+ GHA_ELEVATED_PERMISSIONS_TOKEN: ${{ secrets.GHA_ELEVATED_PERMISSIONS_TOKEN }}
diff --git a/.github/workflows/bump_internal_release.yml b/.github/workflows/bump_internal_release.yml
index 0b5a0de3b6..2609c7daed 100644
--- a/.github/workflows/bump_internal_release.yml
+++ b/.github/workflows/bump_internal_release.yml
@@ -85,7 +85,7 @@ jobs:
BRANCH: ${{ github.ref_name }}
run: |
version="$(cut -d '/' -f 2 <<< "$BRANCH")"
- ./scripts/update_asana_for_release.sh ${{ steps.task-id.outputs.task-id }} "${version}" ${{ vars.MACOS_APP_BOARD_VALIDATION_SECTION_ID }}
+ ./scripts/update_asana_for_release.sh internal ${{ steps.task-id.outputs.task-id }} ${{ vars.MACOS_APP_BOARD_VALIDATION_SECTION_ID }} "${version}"
prepare_release:
name: Prepare Release
diff --git a/.github/workflows/code_freeze.yml b/.github/workflows/code_freeze.yml
index 4663641ea4..d3eb6f886c 100644
--- a/.github/workflows/code_freeze.yml
+++ b/.github/workflows/code_freeze.yml
@@ -82,9 +82,10 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
./scripts/update_asana_for_release.sh \
+ internal \
${{ steps.create_release_task.outputs.asana_task_id }} \
- ${{ steps.create_release_task.outputs.marketing_version }} \
- ${{ vars.MACOS_APP_BOARD_VALIDATION_SECTION_ID }}
+ ${{ vars.MACOS_APP_BOARD_VALIDATION_SECTION_ID }} \
+ ${{ steps.create_release_task.outputs.marketing_version }}
run_tests:
diff --git a/.github/workflows/create_variants.yml b/.github/workflows/create_variants.yml
index 2ec2ebbc33..734d71d8ff 100644
--- a/.github/workflows/create_variants.yml
+++ b/.github/workflows/create_variants.yml
@@ -7,6 +7,50 @@ on:
description: "ATB variants (comma-separated)"
required: true
type: string
+ workflow_call:
+ secrets:
+ BUILD_CERTIFICATE_BASE64:
+ required: true
+ P12_PASSWORD:
+ required: true
+ KEYCHAIN_PASSWORD:
+ required: true
+ REVIEW_PROVISION_PROFILE_BASE64:
+ required: true
+ RELEASE_PROVISION_PROFILE_BASE64:
+ required: true
+ DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64:
+ required: true
+ DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64:
+ required: true
+ NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64_V2:
+ required: true
+ NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64_V2:
+ required: true
+ NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64_V2:
+ required: true
+ NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64_V2:
+ required: true
+ NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64:
+ required: true
+ NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64:
+ required: true
+ APPLE_API_KEY_BASE64:
+ required: true
+ APPLE_API_KEY_ID:
+ required: true
+ APPLE_API_KEY_ISSUER:
+ required: true
+ ASANA_ACCESS_TOKEN:
+ required: true
+ MM_HANDLES_BASE64:
+ required: true
+ MM_WEBHOOK_URL:
+ required: true
+ AWS_ACCESS_KEY_ID_RELEASE_S3:
+ required: true
+ AWS_SECRET_ACCESS_KEY_RELEASE_S3:
+ required: true
jobs:
@@ -24,8 +68,20 @@ jobs:
- name: Set up ATB variants
id: atb-variants
+ env:
+ ASANA_TASK_ID: ${{ vars.DMG_VARIANTS_LIST_TASK_ID }}
+ ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
run: |
- variant_matrix="$(echo "${{ github.event.inputs.atb-variants }}" | sed 's/,/\",\"/g')"
+ atb_variants="${{ github.event.inputs.atb-variants }}"
+ if [[ -z "${atb_variants}" ]]; then
+ atb_variants="$(curl -fSsL "https://app.asana.com/api/1.0/tasks/${ASANA_TASK_ID}?opt_fields=notes" \
+ -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \
+ | jq -r .data.notes \
+ | grep -A1 '^Variants list' \
+ | tail -1)"
+ fi
+ echo "atb-variants=${atb_variants}" >> $GITHUB_ENV
+ variant_matrix="$(sed 's/,/\",\"/g' <<< "${atb_variants}")"
echo "matrix={\"variant\": [\"${variant_matrix}\"]}" >> $GITHUB_OUTPUT
create-atb-variants:
@@ -61,7 +117,7 @@ jobs:
run: |
mkdir -p "${{ env.DEST_DIR }}"
curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/${{ env.DEST_DIR }}/action.yml?ref=${{ github.ref }} --jq .download_url) \
- --output ${{ env.DEST_DIR }}/action.yml
+ --output ${{ env.DEST_DIR }}/action.yml
- name: Install Apple Developer ID Application certificate
uses: ./.github/actions/install-certs-and-profiles
@@ -88,11 +144,11 @@ jobs:
sign_identity="$(security find-certificate -a -c "Developer ID Application" -Z | grep ^SHA-1 | cut -d " " -f3 | uniq)"
/usr/bin/codesign \
- --force \
- --sign ${sign_identity} \
- --options runtime \
- --entitlements entitlements.plist \
- --generate-entitlement-der "DuckDuckGo.app"
+ --force \
+ --sign ${sign_identity} \
+ --options runtime \
+ --entitlements entitlements.plist \
+ --generate-entitlement-der "DuckDuckGo.app"
rm -f entitlements.plist
- name: Notarize the app
@@ -123,14 +179,24 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
curl -fLSs $(gh api https://api.github.com/repos/${{ github.repository }}/contents/scripts/assets/dmg-background.png?ref=${{ github.ref }} --jq .download_url) \
- --output dmg-background.png
- create-dmg --volname "DuckDuckGo" \
+ --output dmg-background.png
+
+ retries=3
+
+ while [[ $retries -gt 0 ]]; do
+ if create-dmg --volname "DuckDuckGo" \
--icon "DuckDuckGo.app" 140 160 \
--background "dmg-background.png" \
--window-size 600 400 \
--icon-size 120 \
--app-drop-link 430 160 "duckduckgo.dmg" \
"dmg"
+ then
+ break
+ fi
+ retries=$((retries-1))
+ done
+
- name: Upload variant DMG
env:
diff --git a/.github/workflows/hotfix.yml b/.github/workflows/hotfix.yml
new file mode 100644
index 0000000000..cd841b32ed
--- /dev/null
+++ b/.github/workflows/hotfix.yml
@@ -0,0 +1,90 @@
+name: Set Up Hotfix Release Branch
+
+on:
+ workflow_dispatch:
+
+jobs:
+
+ create_release_branch:
+
+ name: Create Release Branch
+
+ runs-on: macos-13-xlarge
+ timeout-minutes: 10
+
+ outputs:
+ release_branch_name: ${{ steps.make_release_branch.outputs.release_branch_name }}
+ asana_task_url: ${{ steps.create_release_task.outputs.asana_task_url }}
+
+ steps:
+
+ - name: Assert main branch
+ run: |
+ if [ "${{ github.ref_name }}" != "main" ]; then
+ echo "π Not the main branch"
+ exit 1
+ fi
+
+ - name: Check out the code
+ uses: actions/checkout@v4
+ with:
+ submodules: recursive
+
+ - name: Prepare fastlane
+ run: bundle install
+
+ - name: Make release branch
+ id: make_release_branch
+ env:
+ APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
+ APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
+ APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }}
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ git config --global user.name "Dax the Duck"
+ git config --global user.email "dax@duckduckgo.com"
+ last_release="$(gh api repos/${{ github.repository }}/releases/latest | jq -r .tag_name)"
+ echo "last_release=$last_release" >> $GITHUB_OUTPUT
+ bundle exec fastlane prepare_hotfix version:"$last_release"
+
+ - name: Create release task
+ id: create_release_task
+ env:
+ ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
+ run: |
+ version="$(echo ${{ steps.make_release_branch.outputs.release_branch_name }} | cut -d '/' -f 2)"
+ task_name="macOS App Hotfix Release $version"
+ asana_task_id="$(curl -fLSs -X POST "https://app.asana.com/api/1.0/task_templates/${{ vars.MACOS_HOTFIX_TASK_TEMPLATE_ID }}/instantiateTask" \
+ -H "Authorization: Bearer ${{ env.ASANA_ACCESS_TOKEN }}" \
+ -H "Content-Type: application/json" \
+ -d "{ \"data\": { \"name\": \"$task_name\" }}" \
+ | jq -r .data.new_task.gid)"
+ echo "marketing_version=${version}" >> $GITHUB_OUTPUT
+ echo "asana_task_id=${asana_task_id}" >> $GITHUB_OUTPUT
+ echo "asana_task_url=https://app.asana.com/0/0/${asana_task_id}/f" >> $GITHUB_OUTPUT
+
+ curl -fLSs -X POST "https://app.asana.com/api/1.0/sections/${{ vars.MACOS_APP_DEVELOPMENT_RELEASE_SECTION_ID }}/addTask" \
+ -H "Authorization: Bearer ${{ env.ASANA_ACCESS_TOKEN }}" \
+ -H "Content-Type: application/json" \
+ --output /dev/null \
+ -d "{\"data\": {\"task\": \"${asana_task_id}\"}}"
+
+ assignee_id="$(curl -fLSs https://raw.githubusercontent.com/duckduckgo/BrowserServicesKit/main/.github/actions/asana-failed-pr-checks/user_ids.json \
+ | jq -r .${{ github.actor }})"
+
+ curl -fLSs -X PUT "https://app.asana.com/api/1.0/tasks/${asana_task_id}" \
+ -H "Authorization: Bearer ${{ env.ASANA_ACCESS_TOKEN }}" \
+ -H "Content-Type: application/json" \
+ --output /dev/null \
+ -d "{ \"data\": { \"assignee\": \"$assignee_id\" }}"
+
+ - name: Report success
+ uses: ./.github/actions/asana-add-comment
+ env:
+ BRANCH: ${{ steps.make_release_branch.outputs.release_branch_name }}
+ RELEASE_TAG: ${{ steps.make_release_branch.outputs.last_release }}
+ WORKFLOW_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ with:
+ access-token: ${{ secrets.ASANA_ACCESS_TOKEN }}
+ task-url: ${{ steps.create_release_task.outputs.asana_task_url }}
+ template-name: hotfix-branch-ready
diff --git a/.github/workflows/publish_dmg_release.yml b/.github/workflows/publish_dmg_release.yml
index b749d30a68..a0083f76eb 100644
--- a/.github/workflows/publish_dmg_release.yml
+++ b/.github/workflows/publish_dmg_release.yml
@@ -190,6 +190,22 @@ jobs:
echo "FILES_UPLOADED='No files uploaded.'" >> $GITHUB_ENV
fi
+ - name: Update Asana for the release
+ id: update-asana
+ if: ${{ github.event.inputs.release-type != 'internal' }}
+ continue-on-error: true
+ env:
+ ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
+ BRANCH: ${{ github.ref_name }}
+ run: |
+ version="$(cut -d '/' -f 2 <<< "$BRANCH")"
+ ./scripts/update_asana_for_release.sh public \
+ ${{ steps.task-id.outputs.task-id }} \
+ ${{ vars.MACOS_APP_BOARD_DONE_SECTION_ID }} \
+ "${version}" \
+ announcement-task-contents.txt
+ echo "announcement-task-contents=$(sed 's/"/\\"/g' < announcement-task-contents.txt)" >> $GITHUB_OUTPUT
+
- name: Set common environment variables
if: always()
env:
@@ -235,6 +251,29 @@ jobs:
release-task-url: ${{ github.event.inputs.asana-task-url }}
template-name: ${{ steps.asana-templates.outputs.task-template }}
+ - name: Create Asana task to handle Asana paperwork
+ id: create-asana-paperwork-task
+ if: ${{ steps.update-asana.outcome == 'failure' }}
+ uses: ./.github/actions/asana-create-action-item
+ env:
+ APP_BOARD_ASANA_PROJECT_ID: ${{ vars.MACOS_APP_BOARD_ASANA_PROJECT_ID }}
+ with:
+ access-token: ${{ secrets.ASANA_ACCESS_TOKEN }}
+ release-task-url: ${{ github.event.inputs.asana-task-url }}
+ template-name: update-asana-for-public-release
+
+ - name: Create Asana task to announce the release
+ id: create-announcement-task
+ if: ${{ github.event.inputs.release-type != 'internal' }}
+ uses: ./.github/actions/asana-create-action-item
+ env:
+ html-notes: ${{ steps.update-asana.outputs.announcement-task-contents }}
+ with:
+ access-token: ${{ secrets.ASANA_ACCESS_TOKEN }}
+ html-notes: ${{ env.html-notes }}
+ release-task-url: ${{ github.event.inputs.asana-task-url }}
+ task-name: Announce the release to the company
+
- name: Upload patch to the Asana task
id: upload-patch
if: success()
@@ -266,6 +305,7 @@ jobs:
if: always()
uses: ./.github/actions/asana-log-message
env:
+ ANNOUNCEMENT_TASK_ID: ${{ steps.create-announcement-task.outputs.new-task-id }}
ASSIGNEE_ID: ${{ steps.create-task.outputs.assignee-id }}
TASK_ID: ${{ steps.create-task.outputs.new-task-id }}
with:
@@ -280,3 +320,36 @@ jobs:
access-token: ${{ secrets.ASANA_ACCESS_TOKEN }}
task-url: ${{ github.event.inputs.asana-task-url }}
template-name: ${{ steps.asana-templates.outputs.release-task-comment-template }}
+
+ # This is only run for public and hotfix releases
+ create-variants:
+
+ name: Create DMG Variants
+
+ needs: [publish-to-sparkle]
+
+ if: ${{ github.event.inputs.release-type != 'internal' }}
+
+ uses: ./.github/workflows/create_variants.yml
+ secrets:
+ BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
+ P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
+ KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
+ REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.REVIEW_PROVISION_PROFILE_BASE64 }}
+ RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.RELEASE_PROVISION_PROFILE_BASE64 }}
+ DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_RELEASE_PROVISION_PROFILE_BASE64 }}
+ DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.DBP_AGENT_REVIEW_PROVISION_PROFILE_BASE64 }}
+ NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64_V2: ${{ secrets.NETP_SYSEX_RELEASE_PROVISION_PROFILE_BASE64_V2 }}
+ NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64_V2: ${{ secrets.NETP_SYSEX_REVIEW_PROVISION_PROFILE_BASE64_V2 }}
+ NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64_V2: ${{ secrets.NETP_AGENT_RELEASE_PROVISION_PROFILE_BASE64_V2 }}
+ NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64_V2: ${{ secrets.NETP_AGENT_REVIEW_PROVISION_PROFILE_BASE64_V2 }}
+ NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_RELEASE_PROVISION_PROFILE_BASE64 }}
+ NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64: ${{ secrets.NETP_NOTIFICATIONS_REVIEW_PROVISION_PROFILE_BASE64 }}
+ APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
+ APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
+ APPLE_API_KEY_ISSUER: ${{ secrets.APPLE_API_KEY_ISSUER }}
+ ASANA_ACCESS_TOKEN: ${{ secrets.ASANA_ACCESS_TOKEN }}
+ MM_HANDLES_BASE64: ${{ secrets.MM_HANDLES_BASE64 }}
+ MM_WEBHOOK_URL: ${{ secrets.MM_WEBHOOK_URL }}
+ AWS_ACCESS_KEY_ID_RELEASE_S3: ${{ secrets.AWS_ACCESS_KEY_ID_RELEASE_S3 }}
+ AWS_SECRET_ACCESS_KEY_RELEASE_S3: ${{ secrets.AWS_SECRET_ACCESS_KEY_RELEASE_S3 }}
diff --git a/.github/workflows/tag_release.yml b/.github/workflows/tag_release.yml
index 5a7a1392d2..aeba4860bd 100644
--- a/.github/workflows/tag_release.yml
+++ b/.github/workflows/tag_release.yml
@@ -67,7 +67,8 @@ jobs:
run: |
case "${{ env.BRANCH }}" in
release/*) ;;
- *) echo "π Not a release branch"; exit 1 ;;
+ hotfix/*) ;;
+ *) echo "π Not a release or hotfix branch"; exit 1 ;;
esac
- name: Check out the code
@@ -86,6 +87,7 @@ jobs:
- name: Merge to base branch
id: merge
if: ${{ env.prerelease == 'true' }}
+ continue-on-error: true
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GHA_ELEVATED_PERMISSIONS_TOKEN }}
@@ -100,6 +102,7 @@ jobs:
- name: Delete release branch
id: delete
if: ${{ env.prerelease == 'false' }}
+ continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
run: |
@@ -111,7 +114,9 @@ jobs:
GH_TOKEN: ${{ github.token }}
PROMOTED_TAG: ${{ steps.create-tag.outputs.promoted-tag }}
TAG: ${{ steps.create-tag.outputs.tag }}
+ MERGE_OR_DELETE_FAILED: ${{ (steps.merge.outcome == 'failure') || (steps.delete.outcome == 'failure') }}
run: |
+ echo "MERGE_OR_DELETE_FAILED=${MERGE_OR_DELETE_FAILED}" >> $GITHUB_ENV
echo "TAG=$TAG" >> $GITHUB_ENV
if [[ "${prerelease}" == "true" ]]; then
DMG_VERSION=${TAG//-/.}
@@ -127,18 +132,8 @@ jobs:
echo "LAST_RELEASE_TAG=${last_release_tag}" >> $GITHUB_ENV
fi
- - name: Set up Asana success comment template
- if: success()
- id: asana-success-template
- run: |
- if [[ "${prerelease}" == "true" ]]; then
- echo "comment-template=internal-release-ready" >> $GITHUB_OUTPUT
- else
- echo "comment-template=public-release-tagged" >> $GITHUB_OUTPUT
- fi
-
- name: Set up Asana templates
- if: failure()
+ if: failure() || env.MERGE_OR_DELETE_FAILED == 'true'
id: asana-failure-templates
run: |
if [[ ${{ steps.create-tag.outputs.tag-created }} == "true" ]]; then
@@ -161,7 +156,7 @@ jobs:
- name: Create Asana task on failure
id: create-task-on-failure
- if: failure()
+ if: failure() || env.MERGE_OR_DELETE_FAILED == 'true'
uses: ./.github/actions/asana-create-action-item
with:
access-token: ${{ secrets.ASANA_ACCESS_TOKEN }}
@@ -169,7 +164,7 @@ jobs:
template-name: ${{ steps.asana-failure-templates.outputs.task-template }}
- name: Report failure
- if: failure()
+ if: failure() || env.MERGE_OR_DELETE_FAILED == 'true'
uses: ./.github/actions/asana-log-message
env:
ASSIGNEE_ID: ${{ steps.create-task-on-failure.outputs.assignee-id }}
@@ -179,8 +174,18 @@ jobs:
task-url: ${{ env.asana-task-url }}
template-name: ${{ steps.asana-failure-templates.outputs.comment-template }}
+ - name: Set up Asana success comment template
+ if: ${{ env.MERGE_OR_DELETE_FAILED == 'false' }}
+ id: asana-success-template
+ run: |
+ if [[ "${prerelease}" == "true" ]]; then
+ echo "comment-template=internal-release-ready" >> $GITHUB_OUTPUT
+ else
+ echo "comment-template=public-release-tagged" >> $GITHUB_OUTPUT
+ fi
+
- name: Report success
- if: success()
+ if: ${{ env.MERGE_OR_DELETE_FAILED == 'false' }}
uses: ./.github/actions/asana-log-message
with:
access-token: ${{ secrets.ASANA_ACCESS_TOKEN }}
diff --git a/.gitignore b/.gitignore
index a8169587c9..ccd7c7be5f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -41,6 +41,7 @@ playground.xcworkspace
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
+LocalPackages/*/Package.resolved
.build/
diff --git a/.xcode-version b/.xcode-version
index adbc6d2b1b..dafb659a69 100644
--- a/.xcode-version
+++ b/.xcode-version
@@ -1 +1 @@
-15.1
+15.2
diff --git a/Configuration/BuildNumber.xcconfig b/Configuration/BuildNumber.xcconfig
index 3f4f3b4e21..dcb623008a 100644
--- a/Configuration/BuildNumber.xcconfig
+++ b/Configuration/BuildNumber.xcconfig
@@ -1 +1 @@
-CURRENT_PROJECT_VERSION = 129
+CURRENT_PROJECT_VERSION = 130
diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj
index bffedab9f9..eec7530e9d 100644
--- a/DuckDuckGo.xcodeproj/project.pbxproj
+++ b/DuckDuckGo.xcodeproj/project.pbxproj
@@ -17,7 +17,7 @@
14505A08256084EF00272CC6 /* UserAgent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14505A07256084EF00272CC6 /* UserAgent.swift */; };
1456D6E124EFCBC300775049 /* TabBarCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1456D6E024EFCBC300775049 /* TabBarCollectionView.swift */; };
14D9B8FB24F7E089000D4D13 /* AddressBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14D9B8F924F7E089000D4D13 /* AddressBarViewController.swift */; };
- 1D02633628D8A9A9005CBB41 /* BWEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D02633528D8A9A9005CBB41 /* BWEncryption.m */; };
+ 1D02633628D8A9A9005CBB41 /* BWEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D02633528D8A9A9005CBB41 /* BWEncryption.m */; settings = {COMPILER_FLAGS = "-Wno-deprecated -Wno-strict-prototypes"; }; };
1D074B272909A433006E4AC3 /* PasswordManagerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D074B262909A433006E4AC3 /* PasswordManagerCoordinator.swift */; };
1D12F2E2298BC660009A65FD /* InternalUserDeciderStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D12F2E1298BC660009A65FD /* InternalUserDeciderStoreMock.swift */; };
1D1A33492A6FEB170080ACED /* BurnerMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D1A33482A6FEB170080ACED /* BurnerMode.swift */; };
@@ -380,7 +380,6 @@
3706FB68293F65D500E42796 /* NSNotificationName+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B980E202817604000282EE1 /* NSNotificationName+Debug.swift */; };
3706FB69293F65D500E42796 /* NavigationBarBadgeAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F7F2A5288AD2CA001C0D64 /* NavigationBarBadgeAnimationView.swift */; };
3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4F025D6BF10007F5990 /* AddressBarButton.swift */; };
- 3706FB6B293F65D500E42796 /* HistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527D263B05C600B973F8 /* HistoryEntry.swift */; };
3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5FA69C275F945C00DCE9C9 /* FaviconStore.swift */; };
3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB8203B26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift */; };
3706FB6F293F65D500E42796 /* BookmarkListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292CC2667123700AD2C21 /* BookmarkListViewController.swift */; };
@@ -456,7 +455,7 @@
3706FBC2293F65D500E42796 /* MainMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4BBA3A25C58FA200C4FB0F /* MainMenu.swift */; };
3706FBC5293F65D500E42796 /* CallToAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F21276A32B600DC0649 /* CallToAction.swift */; };
3706FBC6293F65D500E42796 /* MouseOverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693953D26F04BE70015B914 /* MouseOverView.swift */; };
- 3706FBC7293F65D500E42796 /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* HistoryStore.swift */; };
+ 3706FBC7293F65D500E42796 /* EncryptedHistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */; };
3706FBC8293F65D500E42796 /* FirePopoverCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE246F12709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift */; };
3706FBC9293F65D500E42796 /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA61C0D12727F59B00E6B681 /* ArrayExtension.swift */; };
3706FBCA293F65D500E42796 /* CrashReportSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC30A2B268F1ECD00D2D9CD /* CrashReportSender.swift */; };
@@ -496,7 +495,6 @@
3706FBF0293F65D500E42796 /* PasswordManagementItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CC1D7C26A05F250062F04E /* PasswordManagementItemModel.swift */; };
3706FBF2293F65D500E42796 /* FindInPageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A0118125AF60E700FA6A0C /* FindInPageModel.swift */; };
3706FBF3293F65D500E42796 /* PseudoFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92929826670D2A00AD2C21 /* PseudoFolder.swift */; };
- 3706FBF4293F65D500E42796 /* Visit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E91992875B39300AB6B62 /* Visit.swift */; };
3706FBF5293F65D500E42796 /* PixelDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */; };
3706FBF6293F65D500E42796 /* Pixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E45226142B070067D1B9 /* Pixel.swift */; };
3706FBF7293F65D500E42796 /* PixelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E47626146A570067D1B9 /* PixelEvent.swift */; };
@@ -504,7 +502,6 @@
3706FBF9293F65D500E42796 /* BookmarksBarCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE5336A286912D40019DBFD /* BookmarksBarCollectionViewItem.swift */; };
3706FBFA293F65D500E42796 /* FileDownloadError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B23826E742610031CB7F /* FileDownloadError.swift */; };
3706FBFB293F65D500E42796 /* MoreOrLessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E9F27BFE60E0038AD11 /* MoreOrLessView.swift */; };
- 3706FBFD293F65D500E42796 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E46F26146A250067D1B9 /* DateExtension.swift */; };
3706FBFE293F65D500E42796 /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AAE75278263B046100B973F8 /* History.xcdatamodeld */; };
3706FBFF293F65D500E42796 /* PermissionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C853C26944B940048FEBE /* PermissionStore.swift */; };
3706FC00293F65D500E42796 /* PrivacyIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA75A0AD26F3500C0086B667 /* PrivacyIconViewModel.swift */; };
@@ -562,7 +559,6 @@
3706FC38293F65D500E42796 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; };
3706FC3A293F65D500E42796 /* NSOpenPanelExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */; };
3706FC3B293F65D500E42796 /* FirePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE99B8827088A19008B6BD9 /* FirePopover.swift */; };
- 3706FC3C293F65D500E42796 /* HistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */; };
3706FC3E293F65D500E42796 /* VariantManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50372726A12000758A2B /* VariantManager.swift */; };
3706FC3F293F65D500E42796 /* ApplicationDockMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA97BF4525135DD30014931A /* ApplicationDockMenu.swift */; };
3706FC40293F65D500E42796 /* SaveIdentityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B8A4DFE27C83B29005F40E8 /* SaveIdentityViewController.swift */; };
@@ -608,7 +604,6 @@
3706FC6E293F65D500E42796 /* DuckURLSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F28C5228C8EECA00119F70 /* DuckURLSchemeHandler.swift */; };
3706FC6F293F65D500E42796 /* FirePopoverViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA13DCB3271480B0006D48D3 /* FirePopoverViewModel.swift */; };
3706FC71293F65D500E42796 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; };
- 3706FC72293F65D500E42796 /* Stored.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E919B2875C65000AB6B62 /* Stored.swift */; };
3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4F525D6BF2C007F5990 /* AddressBarButtonsViewController.swift */; };
3706FC76293F65D500E42796 /* PixelDataRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C92C32750EF76002AC6B0 /* PixelDataRecord.swift */; };
3706FC77293F65D500E42796 /* PageObserverUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 853014D525E671A000FB8205 /* PageObserverUserScript.swift */; };
@@ -718,7 +713,6 @@
3706FDE0293F661700E42796 /* TabIndexTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D2377B287EBDA300BCE03B /* TabIndexTests.swift */; };
3706FDE1293F661700E42796 /* AdjacentItemEnumeratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534CA42811987D002621E7 /* AdjacentItemEnumeratorTests.swift */; };
3706FDE2293F661700E42796 /* PixelArgumentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44222616CABC00DD1EC2 /* PixelArgumentsTests.swift */; };
- 3706FDE3293F661700E42796 /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AAE75278263B046100B973F8 /* History.xcdatamodeld */; };
3706FDE4293F661700E42796 /* TabLazyLoaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37534C9D28104D9B002621E7 /* TabLazyLoaderTests.swift */; };
3706FDE5293F661700E42796 /* URLEventHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F1B0C825EF9759004792B6 /* URLEventHandlerTests.swift */; };
3706FDE6293F661700E42796 /* BookmarkOutlineViewDataSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9292B32667103000AD2C21 /* BookmarkOutlineViewDataSourceTests.swift */; };
@@ -1469,7 +1463,7 @@
4B957A012AC7AE700062CA31 /* SafariDataImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BB99CFD26FE191E001E4761 /* SafariDataImporter.swift */; };
4B957A022AC7AE700062CA31 /* WaitlistViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB00C2A983B24000927DB /* WaitlistViewModel.swift */; };
4B957A032AC7AE700062CA31 /* LocalBookmarkStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 987799F829999973005D8EB6 /* LocalBookmarkStore.swift */; };
- 4B957A042AC7AE700062CA31 /* BWEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D02633528D8A9A9005CBB41 /* BWEncryption.m */; };
+ 4B957A042AC7AE700062CA31 /* BWEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D02633528D8A9A9005CBB41 /* BWEncryption.m */; settings = {COMPILER_FLAGS = "-Wno-deprecated -Wno-strict-prototypes"; }; };
4B957A052AC7AE700062CA31 /* StatisticsLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50342726A11F00758A2B /* StatisticsLoader.swift */; };
4B957A072AC7AE700062CA31 /* PrivacyPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CD54C127F2FDD100F1F7B9 /* PrivacyPreferencesModel.swift */; };
4B957A082AC7AE700062CA31 /* LocalUnprotectedDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */; };
@@ -1559,7 +1553,6 @@
4B957A602AC7AE700062CA31 /* NSNotificationName+Debug.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B980E202817604000282EE1 /* NSNotificationName+Debug.swift */; };
4B957A612AC7AE700062CA31 /* NavigationBarBadgeAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31F7F2A5288AD2CA001C0D64 /* NavigationBarBadgeAnimationView.swift */; };
4B957A622AC7AE700062CA31 /* AddressBarButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4F025D6BF10007F5990 /* AddressBarButton.swift */; };
- 4B957A632AC7AE700062CA31 /* HistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527D263B05C600B973F8 /* HistoryEntry.swift */; };
4B957A642AC7AE700062CA31 /* FaviconStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA5FA69C275F945C00DCE9C9 /* FaviconStore.swift */; };
4B957A652AC7AE700062CA31 /* WaitlistTermsAndConditionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB0142A983B24000927DB /* WaitlistTermsAndConditionsView.swift */; };
4B957A662AC7AE700062CA31 /* SuggestionListCharacteristics.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAB8203B26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift */; };
@@ -1664,7 +1657,7 @@
4B957ACE2AC7AE700062CA31 /* BrowserTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA585D83248FD31100E9A3E2 /* BrowserTabViewController.swift */; };
4B957ACF2AC7AE700062CA31 /* CallToAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85707F21276A32B600DC0649 /* CallToAction.swift */; };
4B957AD02AC7AE700062CA31 /* MouseOverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B693953D26F04BE70015B914 /* MouseOverView.swift */; };
- 4B957AD12AC7AE700062CA31 /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* HistoryStore.swift */; };
+ 4B957AD12AC7AE700062CA31 /* EncryptedHistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */; };
4B957AD22AC7AE700062CA31 /* FirePopoverCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE246F12709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift */; };
4B957AD32AC7AE700062CA31 /* ArrayExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA61C0D12727F59B00E6B681 /* ArrayExtension.swift */; };
4B957AD42AC7AE700062CA31 /* NetworkProtectionInviteCodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B4D60702A0B29FA00BCD287 /* NetworkProtectionInviteCodeViewModel.swift */; };
@@ -1717,7 +1710,6 @@
4B957B042AC7AE700062CA31 /* UpdateController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAD86E51267A0DFF005C11BE /* UpdateController.swift */; };
4B957B052AC7AE700062CA31 /* FindInPageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85A0118125AF60E700FA6A0C /* FindInPageModel.swift */; };
4B957B062AC7AE700062CA31 /* PseudoFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B92929826670D2A00AD2C21 /* PseudoFolder.swift */; };
- 4B957B072AC7AE700062CA31 /* Visit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E91992875B39300AB6B62 /* Visit.swift */; };
4B957B082AC7AE700062CA31 /* PixelDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6DA44012616B28300DD1EC2 /* PixelDataStore.swift */; };
4B957B092AC7AE700062CA31 /* WaitlistStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B9DB00E2A983B24000927DB /* WaitlistStorage.swift */; };
4B957B0A2AC7AE700062CA31 /* Pixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E45226142B070067D1B9 /* Pixel.swift */; };
@@ -1727,7 +1719,6 @@
4B957B0E2AC7AE700062CA31 /* BookmarksBarCollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BE5336A286912D40019DBFD /* BookmarksBarCollectionViewItem.swift */; };
4B957B0F2AC7AE700062CA31 /* FileDownloadError.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C0B23826E742610031CB7F /* FileDownloadError.swift */; };
4B957B102AC7AE700062CA31 /* MoreOrLessView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85589E9F27BFE60E0038AD11 /* MoreOrLessView.swift */; };
- 4B957B112AC7AE700062CA31 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E46F26146A250067D1B9 /* DateExtension.swift */; };
4B957B122AC7AE700062CA31 /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AAE75278263B046100B973F8 /* History.xcdatamodeld */; };
4B957B132AC7AE700062CA31 /* PermissionStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C853C26944B940048FEBE /* PermissionStore.swift */; };
4B957B142AC7AE700062CA31 /* PrivacyIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA75A0AD26F3500C0086B667 /* PrivacyIconViewModel.swift */; };
@@ -1794,7 +1785,6 @@
4B957B512AC7AE700062CA31 /* BundleExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6106B9D26A565DA0013B453 /* BundleExtension.swift */; };
4B957B522AC7AE700062CA31 /* NSOpenPanelExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0511DF262CAA8600F6079C /* NSOpenPanelExtensions.swift */; };
4B957B532AC7AE700062CA31 /* FirePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE99B8827088A19008B6BD9 /* FirePopover.swift */; };
- 4B957B542AC7AE700062CA31 /* HistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */; };
4B957B552AC7AE700062CA31 /* NetworkProtectionOnboardingMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B05829D2A812AC000AC3F7C /* NetworkProtectionOnboardingMenu.swift */; };
4B957B562AC7AE700062CA31 /* VariantManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69B50372726A12000758A2B /* VariantManager.swift */; };
4B957B572AC7AE700062CA31 /* ApplicationDockMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA97BF4525135DD30014931A /* ApplicationDockMenu.swift */; };
@@ -1856,7 +1846,6 @@
4B957B932AC7AE700062CA31 /* FirePopoverViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA13DCB3271480B0006D48D3 /* FirePopoverViewModel.swift */; };
4B957B942AC7AE700062CA31 /* BWCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D43EB37292B636E0065E5D6 /* BWCommand.swift */; };
4B957B952AC7AE700062CA31 /* NSColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41D174025CB131900472416 /* NSColorExtension.swift */; };
- 4B957B962AC7AE700062CA31 /* Stored.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E919B2875C65000AB6B62 /* Stored.swift */; };
4B957B972AC7AE700062CA31 /* AddressBarButtonsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAC5E4F525D6BF2C007F5990 /* AddressBarButtonsViewController.swift */; };
4B957B982AC7AE700062CA31 /* BWError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DDF076028F815AD00EDFBE3 /* BWError.swift */; };
4B957B9A2AC7AE700062CA31 /* PixelDataRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = B68C92C32750EF76002AC6B0 /* PixelDataRecord.swift */; };
@@ -2359,10 +2348,16 @@
85C6A29625CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */; };
85CC1D7B26A05ECF0062F04E /* PasswordManagementItemListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CC1D7A26A05ECF0062F04E /* PasswordManagementItemListModel.swift */; };
85CC1D7D26A05F250062F04E /* PasswordManagementItemModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85CC1D7C26A05F250062F04E /* PasswordManagementItemModel.swift */; };
+ 85D0327B2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */; };
+ 85D0327C2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */; };
+ 85D0327D2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */; };
85D33F1225C82EB3002B91A6 /* ConfigurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D33F1125C82EB3002B91A6 /* ConfigurationManager.swift */; };
85D438B6256E7C9E00F3BAF8 /* ContextMenuUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D438B5256E7C9E00F3BAF8 /* ContextMenuUserScript.swift */; };
85D885B026A590A90077C374 /* NSNotificationName+PasswordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D885AF26A590A90077C374 /* NSNotificationName+PasswordManager.swift */; };
85D885B326A5A9DE0077C374 /* NSAlert+PasswordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D885B226A5A9DE0077C374 /* NSAlert+PasswordManager.swift */; };
+ 85E2BBCE2B8F534000DBEC7A /* History in Frameworks */ = {isa = PBXBuildFile; productRef = 85E2BBCD2B8F534000DBEC7A /* History */; };
+ 85E2BBD02B8F534A00DBEC7A /* History in Frameworks */ = {isa = PBXBuildFile; productRef = 85E2BBCF2B8F534A00DBEC7A /* History */; };
+ 85E2BBD22B8F536F00DBEC7A /* History in Frameworks */ = {isa = PBXBuildFile; productRef = 85E2BBD12B8F536F00DBEC7A /* History */; };
85F0FF1327CFAB04001C7C6E /* RecentlyVisitedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F0FF1227CFAB04001C7C6E /* RecentlyVisitedView.swift */; };
85F1B0C925EF9759004792B6 /* URLEventHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F1B0C825EF9759004792B6 /* URLEventHandlerTests.swift */; };
85F487B5276A8F2E003CE668 /* OnboardingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F487B4276A8F2E003CE668 /* OnboardingTests.swift */; };
@@ -2493,8 +2488,6 @@
AA75A0AE26F3500C0086B667 /* PrivacyIconViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA75A0AD26F3500C0086B667 /* PrivacyIconViewModel.swift */; };
AA7E9176286DB05D00AB6B62 /* RecentlyClosedCoordinatorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E9175286DB05D00AB6B62 /* RecentlyClosedCoordinatorMock.swift */; };
AA7E919728746BCC00AB6B62 /* HistoryMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E919628746BCC00AB6B62 /* HistoryMenu.swift */; };
- AA7E919A2875B39300AB6B62 /* Visit.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E91992875B39300AB6B62 /* Visit.swift */; };
- AA7E919C2875C65000AB6B62 /* Stored.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E919B2875C65000AB6B62 /* Stored.swift */; };
AA7E919F287872EA00AB6B62 /* VisitViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7E919E287872EA00AB6B62 /* VisitViewModel.swift */; };
AA7EB6DF27E7C57D00036718 /* MouseOverAnimationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7EB6DE27E7C57D00036718 /* MouseOverAnimationButton.swift */; };
AA7EB6E227E7D05500036718 /* flame-mouse-over.json in Resources */ = {isa = PBXBuildFile; fileRef = AA7EB6E027E7D05500036718 /* flame-mouse-over.json */; };
@@ -2572,16 +2565,13 @@
AAE246F8270A406200BEEAEE /* FirePopoverCollectionViewHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE246F7270A406200BEEAEE /* FirePopoverCollectionViewHeader.swift */; };
AAE39D1B24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE39D1A24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift */; };
AAE7527A263B046100B973F8 /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AAE75278263B046100B973F8 /* History.xcdatamodeld */; };
- AAE7527C263B056C00B973F8 /* HistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* HistoryStore.swift */; };
- AAE7527E263B05C600B973F8 /* HistoryEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527D263B05C600B973F8 /* HistoryEntry.swift */; };
- AAE75280263B0A4D00B973F8 /* HistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */; };
+ AAE7527C263B056C00B973F8 /* EncryptedHistoryStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */; };
AAE8B110258A456C00E81239 /* TabPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */; };
AAE99B8927088A19008B6BD9 /* FirePopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAE99B8827088A19008B6BD9 /* FirePopover.swift */; };
AAEC74B22642C57200C2EFBC /* HistoryCoordinatingMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */; };
AAEC74B42642C69300C2EFBC /* HistoryCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B32642C69300C2EFBC /* HistoryCoordinatorTests.swift */; };
AAEC74B62642CC6A00C2EFBC /* HistoryStoringMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B52642CC6A00C2EFBC /* HistoryStoringMock.swift */; };
AAEC74B82642E43800C2EFBC /* HistoryStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEC74B72642E43800C2EFBC /* HistoryStoreTests.swift */; };
- AAEC74BC2642F0F800C2EFBC /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = AAE75278263B046100B973F8 /* History.xcdatamodeld */; };
AAECA42024EEA4AC00EFA63A /* IndexPathExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAECA41F24EEA4AC00EFA63A /* IndexPathExtension.swift */; };
AAEEC6A927088ADB008445F7 /* FireCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEEC6A827088ADB008445F7 /* FireCoordinator.swift */; };
AAEF6BC8276A081C0024DCF4 /* FaviconSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAEF6BC7276A081C0024DCF4 /* FaviconSelector.swift */; };
@@ -2728,6 +2718,8 @@
B64C85422694590B0048FEBE /* PermissionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64C85412694590B0048FEBE /* PermissionButton.swift */; };
B64CE01E2B8622D700126CA5 /* AddressBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64CE01D2B8622D700126CA5 /* AddressBarTests.swift */; };
B64CE01F2B8622D700126CA5 /* AddressBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B64CE01D2B8622D700126CA5 /* AddressBarTests.swift */; };
+ B64E42AB2B909DC9006C1346 /* test.pdf in Resources */ = {isa = PBXBuildFile; fileRef = B64E42AA2B909DC9006C1346 /* test.pdf */; };
+ B64E42AC2B909DC9006C1346 /* test.pdf in Resources */ = {isa = PBXBuildFile; fileRef = B64E42AA2B909DC9006C1346 /* test.pdf */; };
B65211252B29A42C00B30633 /* BookmarkStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA652CDA25DDAB32009059CC /* BookmarkStoreMock.swift */; };
B65211262B29A42E00B30633 /* BookmarkStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA652CDA25DDAB32009059CC /* BookmarkStoreMock.swift */; };
B65211272B29A43000B30633 /* BookmarkStoreMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA652CDA25DDAB32009059CC /* BookmarkStoreMock.swift */; };
@@ -2744,6 +2736,9 @@
B658BAB62B0F845D00D1F2C7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */; };
B658BAB72B0F848D00D1F2C7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */; };
B658BAB92B0F849100D1F2C7 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */; };
+ B65C7DFB2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */; };
+ B65C7DFC2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */; };
+ B65C7DFD2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */; };
B65CD8CB2B316DF100A595BB /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = B65CD8CA2B316DF100A595BB /* SnapshotTesting */; };
B65CD8CD2B316DFC00A595BB /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = B65CD8CC2B316DFC00A595BB /* SnapshotTesting */; };
B65CD8CF2B316E0200A595BB /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = B65CD8CE2B316E0200A595BB /* SnapshotTesting */; };
@@ -2902,7 +2897,6 @@
B6A924D92664C72E001A28CA /* WebKitDownloadTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A924D82664C72D001A28CA /* WebKitDownloadTask.swift */; };
B6A9E45326142B070067D1B9 /* Pixel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E45226142B070067D1B9 /* Pixel.swift */; };
B6A9E46B2614618A0067D1B9 /* OperatingSystemVersionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E46A2614618A0067D1B9 /* OperatingSystemVersionExtension.swift */; };
- B6A9E47026146A250067D1B9 /* DateExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E46F26146A250067D1B9 /* DateExtension.swift */; };
B6A9E47726146A570067D1B9 /* PixelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E47626146A570067D1B9 /* PixelEvent.swift */; };
B6A9E47F26146A800067D1B9 /* PixelArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E47E26146A800067D1B9 /* PixelArguments.swift */; };
B6A9E48426146AAB0067D1B9 /* PixelParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6A9E48326146AAB0067D1B9 /* PixelParameters.swift */; };
@@ -3030,6 +3024,8 @@
B6EC37FC29B83E99001ACE79 /* TestsURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */; };
B6EC37FD29B83E99001ACE79 /* TestsURLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */; };
B6EC37FF29B8D915001ACE79 /* Configuration in Frameworks */ = {isa = PBXBuildFile; productRef = B6EC37FE29B8D915001ACE79 /* Configuration */; };
+ B6EEDD7D2B8C69E900637EBC /* TabContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */; };
+ B6EEDD7E2B8C69E900637EBC /* TabContentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */; };
B6F1C80B2761C45400334924 /* LocalUnprotectedDomains.swift in Sources */ = {isa = PBXBuildFile; fileRef = 336B39E22726B4B700C417D3 /* LocalUnprotectedDomains.swift */; };
B6F41031264D2B23003DA42C /* ProgressExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F41030264D2B23003DA42C /* ProgressExtension.swift */; };
B6F56567299A414300A04298 /* WKWebViewMockingExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */; };
@@ -3922,6 +3918,7 @@
85C6A29525CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsWrapper.swift; sourceTree = ""; };
85CC1D7A26A05ECF0062F04E /* PasswordManagementItemListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordManagementItemListModel.swift; sourceTree = ""; };
85CC1D7C26A05F250062F04E /* PasswordManagementItemModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordManagementItemModel.swift; sourceTree = ""; };
+ 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinatorExtension.swift; sourceTree = ""; };
85D33F1125C82EB3002B91A6 /* ConfigurationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationManager.swift; sourceTree = ""; };
85D438B5256E7C9E00F3BAF8 /* ContextMenuUserScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuUserScript.swift; sourceTree = ""; };
85D885AF26A590A90077C374 /* NSNotificationName+PasswordManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSNotificationName+PasswordManager.swift"; sourceTree = ""; };
@@ -4044,8 +4041,6 @@
AA7E9175286DB05D00AB6B62 /* RecentlyClosedCoordinatorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentlyClosedCoordinatorMock.swift; sourceTree = ""; };
AA7E919628746BCC00AB6B62 /* HistoryMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryMenu.swift; sourceTree = ""; };
AA7E91982875AB4700AB6B62 /* History 6.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "History 6.xcdatamodel"; sourceTree = ""; };
- AA7E91992875B39300AB6B62 /* Visit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Visit.swift; sourceTree = ""; };
- AA7E919B2875C65000AB6B62 /* Stored.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stored.swift; sourceTree = ""; };
AA7E919E287872EA00AB6B62 /* VisitViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisitViewModel.swift; sourceTree = ""; };
AA7EB6DE27E7C57D00036718 /* MouseOverAnimationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MouseOverAnimationButton.swift; sourceTree = ""; };
AA7EB6E027E7D05500036718 /* flame-mouse-over.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "flame-mouse-over.json"; sourceTree = ""; };
@@ -4125,9 +4120,7 @@
AAE246F7270A406200BEEAEE /* FirePopoverCollectionViewHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopoverCollectionViewHeader.swift; sourceTree = ""; };
AAE39D1A24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCollectionViewModelDelegateMock.swift; sourceTree = ""; };
AAE75279263B046100B973F8 /* History.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = History.xcdatamodel; sourceTree = ""; };
- AAE7527B263B056C00B973F8 /* HistoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryStore.swift; sourceTree = ""; };
- AAE7527D263B05C600B973F8 /* HistoryEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryEntry.swift; sourceTree = ""; };
- AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinator.swift; sourceTree = ""; };
+ AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedHistoryStore.swift; sourceTree = ""; };
AAE8B10F258A456C00E81239 /* TabPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabPreviewViewController.swift; sourceTree = ""; };
AAE99B8827088A19008B6BD9 /* FirePopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirePopover.swift; sourceTree = ""; };
AAEC74B12642C57200C2EFBC /* HistoryCoordinatingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinatingMock.swift; sourceTree = ""; };
@@ -4226,6 +4219,7 @@
B64C853C26944B940048FEBE /* PermissionStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionStore.swift; sourceTree = ""; };
B64C85412694590B0048FEBE /* PermissionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionButton.swift; sourceTree = ""; };
B64CE01D2B8622D700126CA5 /* AddressBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressBarTests.swift; sourceTree = ""; };
+ B64E42AA2B909DC9006C1346 /* test.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = test.pdf; sourceTree = ""; };
B65349A9265CF45000DCC645 /* DispatchQueueExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchQueueExtensionsTests.swift; sourceTree = ""; };
B6553691268440D700085A79 /* WKProcessPool+GeolocationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WKProcessPool+GeolocationProvider.swift"; sourceTree = ""; };
B65536962684413900085A79 /* WKGeolocationProvider.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKGeolocationProvider.h; sourceTree = ""; };
@@ -4237,6 +4231,7 @@
B657841925FA484B00D8DB33 /* NSException+Catch.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSException+Catch.m"; sourceTree = ""; };
B657841E25FA497600D8DB33 /* NSException+Catch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSException+Catch.swift"; sourceTree = ""; };
B658BAB52B0F845D00D1F2C7 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; };
+ B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WKPDFHUDViewWrapper.swift; sourceTree = ""; };
B65CD8D42B316FCA00A595BB /* __Snapshots__ */ = {isa = PBXFileReference; lastKnownFileType = folder; path = __Snapshots__; sourceTree = ""; };
B65CD8D72B341FD300A595BB /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
B65E6B9D26D9EC0800095F96 /* CircularProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; };
@@ -4339,7 +4334,6 @@
B6A924D82664C72D001A28CA /* WebKitDownloadTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitDownloadTask.swift; sourceTree = ""; };
B6A9E45226142B070067D1B9 /* Pixel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pixel.swift; sourceTree = ""; };
B6A9E46A2614618A0067D1B9 /* OperatingSystemVersionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperatingSystemVersionExtension.swift; sourceTree = ""; };
- B6A9E46F26146A250067D1B9 /* DateExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateExtension.swift; sourceTree = ""; };
B6A9E47626146A570067D1B9 /* PixelEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelEvent.swift; sourceTree = ""; };
B6A9E47E26146A800067D1B9 /* PixelArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelArguments.swift; sourceTree = ""; };
B6A9E48326146AAB0067D1B9 /* PixelParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PixelParameters.swift; sourceTree = ""; };
@@ -4420,6 +4414,7 @@
B6EC37EA29B5DA2A001ACE79 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; };
B6EC37FA29B6447F001ACE79 /* TestsServer.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = TestsServer.xcconfig; sourceTree = ""; };
B6EC37FB29B83E99001ACE79 /* TestsURLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestsURLExtension.swift; sourceTree = ""; };
+ B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabContentTests.swift; sourceTree = ""; };
B6F41030264D2B23003DA42C /* ProgressExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtension.swift; sourceTree = ""; };
B6F56566299A414300A04298 /* WKWebViewMockingExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WKWebViewMockingExtension.swift; sourceTree = ""; };
B6F7127D29F6779000594A45 /* QRSharingService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRSharingService.swift; sourceTree = ""; };
@@ -4493,6 +4488,7 @@
372217822B33380700B8E9C2 /* TestUtils in Frameworks */,
3706FCAA293F65D500E42796 /* UserScript in Frameworks */,
3706FCAB293F65D500E42796 /* TrackerRadarKit in Frameworks */,
+ 85E2BBD02B8F534A00DBEC7A /* History in Frameworks */,
4BF97AD52B43C43F00EB4240 /* NetworkProtection in Frameworks */,
3739326529AE4B39009346AE /* DDGSync in Frameworks */,
37DF000729F9C061002B7D3E /* SyncDataProviders in Frameworks */,
@@ -4626,6 +4622,7 @@
1E21F8E32B73E48600FB272E /* Subscription in Frameworks */,
4B957BE42AC7AE700062CA31 /* DDGSync in Frameworks */,
4B957BE52AC7AE700062CA31 /* OpenSSL in Frameworks */,
+ 85E2BBD22B8F536F00DBEC7A /* History in Frameworks */,
4B957BE62AC7AE700062CA31 /* PrivacyDashboard in Frameworks */,
7B8C083C2AE1268E00F4C67F /* PixelKit in Frameworks */,
4B957BE72AC7AE700062CA31 /* SyncDataProviders in Frameworks */,
@@ -4688,6 +4685,7 @@
buildActionMask = 2147483647;
files = (
373FB4B12B4D6C42004C88D6 /* PreferencesViews in Frameworks */,
+ 85E2BBCE2B8F534000DBEC7A /* History in Frameworks */,
1EA7B8D32B7E078C000330A4 /* SubscriptionUI in Frameworks */,
B6F7128129F681EB00594A45 /* QuickLookUI.framework in Frameworks */,
9DB6E7242AA0DC5800A17F3C /* LoginItems in Frameworks */,
@@ -5621,7 +5619,6 @@
children = (
4B677440255DBEEA00025BD8 /* Database.swift */,
B6085D052743905F00A9C456 /* CoreDataStore.swift */,
- AA7E919B2875C65000AB6B62 /* Stored.swift */,
);
path = Database;
sourceTree = "";
@@ -7583,7 +7580,6 @@
4BA1A6C1258B0A1300F6F690 /* ContiguousBytesExtension.swift */,
B603FD9D2A02712E00F3FCA9 /* CIImageExtension.swift */,
85AC3AF625D5DBFD00C7D2AA /* DataExtension.swift */,
- B6A9E46F26146A250067D1B9 /* DateExtension.swift */,
B6040855274B830F00680351 /* DictionaryExtension.swift */,
B63D467025BFA6C100874977 /* DispatchQueueExtensions.swift */,
4B3B848F297A0E1000A384BD /* EmailManagerExtension.swift */,
@@ -7646,6 +7642,7 @@
B602E7CE2A93A5FF00F12201 /* WKBackForwardListExtension.swift */,
B68412242B6A67920092F66A /* WKBackForwardListItemExtension.swift */,
B6DA06E7291401D700225DE2 /* WKMenuItemIdentifier.swift */,
+ B65C7DFA2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift */,
B645D8F529FA95440024461F /* WKProcessPoolExtension.swift */,
AAA0CC69253CC43C0079BC96 /* WKUserContentControllerExtension.swift */,
B63D466725BEB6C200874977 /* WKWebView+Private.h */,
@@ -7696,7 +7693,6 @@
isa = PBXGroup;
children = (
AA7E919D287872DB00AB6B62 /* ViewModel */,
- AAE75277263B038F00B973F8 /* Model */,
AAE75276263B038A00B973F8 /* Services */,
);
path = History;
@@ -7706,21 +7702,12 @@
isa = PBXGroup;
children = (
AAE75278263B046100B973F8 /* History.xcdatamodeld */,
- AAE7527B263B056C00B973F8 /* HistoryStore.swift */,
+ AAE7527B263B056C00B973F8 /* EncryptedHistoryStore.swift */,
+ 85D0327A2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift */,
);
path = Services;
sourceTree = "";
};
- AAE75277263B038F00B973F8 /* Model */ = {
- isa = PBXGroup;
- children = (
- AAE7527F263B0A4D00B973F8 /* HistoryCoordinator.swift */,
- AAE7527D263B05C600B973F8 /* HistoryEntry.swift */,
- AA7E91992875B39300AB6B62 /* Visit.swift */,
- );
- path = Model;
- sourceTree = "";
- };
AAE8B0FD258A416F00E81239 /* TabPreview */ = {
isa = PBXGroup;
children = (
@@ -7892,8 +7879,10 @@
B644B43C29D56811003FA9AB /* Tab */ = {
isa = PBXGroup;
children = (
+ B64E42AA2B909DC9006C1346 /* test.pdf */,
B644B43929D565DB003FA9AB /* SearchNonexistentDomainTests.swift */,
B693766D2B6B5F26005BD9D4 /* ErrorPageTests.swift */,
+ B6EEDD7C2B8C69E900637EBC /* TabContentTests.swift */,
B64CE01D2B8622D700126CA5 /* AddressBarTests.swift */,
);
path = Tab;
@@ -8354,6 +8343,7 @@
373FB4B22B4D6C4B004C88D6 /* PreferencesViews */,
312978892B64131200B67619 /* DataBrokerProtection */,
7B1459562B7D43E500047F2C /* NetworkProtectionProxy */,
+ 85E2BBCF2B8F534A00DBEC7A /* History */,
);
productName = DuckDuckGo;
productReference = 3706FD05293F65D500E42796 /* DuckDuckGo App Store.app */;
@@ -8635,6 +8625,7 @@
373FB4B42B4D6C57004C88D6 /* PreferencesViews */,
1E21F8E22B73E48600FB272E /* Subscription */,
7B94E1642B7ED95100E32B96 /* NetworkProtectionProxy */,
+ 85E2BBD12B8F536F00DBEC7A /* History */,
);
productName = DuckDuckGo;
productReference = 4B957C412AC7AE700062CA31 /* DuckDuckGo Privacy Pro.app */;
@@ -8797,6 +8788,7 @@
3722177F2B3337FE00B8E9C2 /* TestUtils */,
373FB4B02B4D6C42004C88D6 /* PreferencesViews */,
7BA076BA2B65D61400D7FB72 /* NetworkProtectionProxy */,
+ 85E2BBCD2B8F534000DBEC7A /* History */,
1EA7B8D22B7E078C000330A4 /* SubscriptionUI */,
1EA7B8D42B7E078C000330A4 /* Subscription */,
);
@@ -8856,7 +8848,7 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1520;
- LastUpgradeCheck = 1400;
+ LastUpgradeCheck = 1520;
ORGANIZATIONNAME = DuckDuckGo;
TargetAttributes = {
3706FDD3293F661700E42796 = {
@@ -9044,6 +9036,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ B64E42AC2B909DC9006C1346 /* test.pdf in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -9058,6 +9051,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ B64E42AB2B909DC9006C1346 /* test.pdf in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -9289,6 +9283,7 @@
/* Begin PBXShellScriptBuildPhase section */
3121F62B2B64266A002F706A /* Copy Swift Package resources */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -9383,6 +9378,7 @@
};
4B2D067D2A13341200DE1F49 /* ShellScript */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -9457,6 +9453,7 @@
};
4BBA2D272B6AC09D00F6A470 /* Embed Login Items */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -9513,6 +9510,7 @@
};
7B557F2A2B8CA2A400099746 /* Embed Debug-only Network Extensions */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -9761,6 +9759,7 @@
1D36E659298AA3BA00AA485D /* InternalUserDeciderStore.swift in Sources */,
B6BCC5242AFCDABB002C5499 /* DataImportSourceViewModel.swift in Sources */,
3706FEBC293F6EFF00E42796 /* BWResponse.swift in Sources */,
+ B65C7DFC2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */,
3706FAF4293F65D500E42796 /* SafariBookmarksReader.swift in Sources */,
31C9ADE62AF0564500CEF57D /* WaitlistFeatureSetupHandler.swift in Sources */,
B65211262B29A42E00B30633 /* BookmarkStoreMock.swift in Sources */,
@@ -9911,7 +9910,6 @@
1D1A334A2A6FEB170080ACED /* BurnerMode.swift in Sources */,
B603971B29BA084C00902A34 /* JSAlertController.swift in Sources */,
3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */,
- 3706FB6B293F65D500E42796 /* HistoryEntry.swift in Sources */,
4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */,
3706FB6C293F65D500E42796 /* FaviconStore.swift in Sources */,
3706FB6D293F65D500E42796 /* SuggestionListCharacteristics.swift in Sources */,
@@ -10046,7 +10044,7 @@
56D145EC29E6C99B00E3488A /* DataImportStatusProviding.swift in Sources */,
85774B002A713D3B00DE0561 /* BookmarksBarMenuFactory.swift in Sources */,
B602E81E2A1E25B1006D261F /* NEOnDemandRuleExtension.swift in Sources */,
- 3706FBC7293F65D500E42796 /* HistoryStore.swift in Sources */,
+ 3706FBC7293F65D500E42796 /* EncryptedHistoryStore.swift in Sources */,
3706FBC8293F65D500E42796 /* FirePopoverCollectionViewItem.swift in Sources */,
3706FBC9293F65D500E42796 /* ArrayExtension.swift in Sources */,
3706FBCA293F65D500E42796 /* CrashReportSender.swift in Sources */,
@@ -10111,7 +10109,6 @@
1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */,
3706FBF3293F65D500E42796 /* PseudoFolder.swift in Sources */,
1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */,
- 3706FBF4293F65D500E42796 /* Visit.swift in Sources */,
3706FBF5293F65D500E42796 /* PixelDataStore.swift in Sources */,
3706FBF6293F65D500E42796 /* Pixel.swift in Sources */,
3706FBF7293F65D500E42796 /* PixelEvent.swift in Sources */,
@@ -10121,7 +10118,6 @@
3706FBFA293F65D500E42796 /* FileDownloadError.swift in Sources */,
379E877729E98729001C8BB0 /* BookmarksCleanupErrorHandling.swift in Sources */,
3706FBFB293F65D500E42796 /* MoreOrLessView.swift in Sources */,
- 3706FBFD293F65D500E42796 /* DateExtension.swift in Sources */,
987799FA29999973005D8EB6 /* LocalBookmarkStore.swift in Sources */,
B602E7D02A93A5FF00F12201 /* WKBackForwardListExtension.swift in Sources */,
3706FBFE293F65D500E42796 /* History.xcdatamodeld in Sources */,
@@ -10211,7 +10207,6 @@
3706FC3A293F65D500E42796 /* NSOpenPanelExtensions.swift in Sources */,
3706FC3B293F65D500E42796 /* FirePopover.swift in Sources */,
4B4D60C12A0C848E00BCD287 /* NetworkProtectionControllerErrorStore.swift in Sources */,
- 3706FC3C293F65D500E42796 /* HistoryCoordinator.swift in Sources */,
3706FC3E293F65D500E42796 /* VariantManager.swift in Sources */,
3706FC3F293F65D500E42796 /* ApplicationDockMenu.swift in Sources */,
B68412152B694BA10092F66A /* NSObject+performSelector.m in Sources */,
@@ -10221,6 +10216,7 @@
1DB67F2E2B6FEFDB003DF243 /* ViewSnapshotRenderer.swift in Sources */,
3706FC42293F65D500E42796 /* PixelArguments.swift in Sources */,
3706FC43293F65D500E42796 /* PinnedTabsViewModel.swift in Sources */,
+ 85D0327C2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */,
B6685E4329A61C470043D2EE /* DownloadsTabExtension.swift in Sources */,
3706FC44293F65D500E42796 /* BookmarkList.swift in Sources */,
3706FC45293F65D500E42796 /* BookmarkTableRowView.swift in Sources */,
@@ -10280,7 +10276,6 @@
37445F9A2A1566420029F789 /* SyncDataProviders.swift in Sources */,
3706FC6F293F65D500E42796 /* FirePopoverViewModel.swift in Sources */,
3706FC71293F65D500E42796 /* NSColorExtension.swift in Sources */,
- 3706FC72293F65D500E42796 /* Stored.swift in Sources */,
1DB9618229F67F6100CF5568 /* FaviconNullStore.swift in Sources */,
3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */,
3706FC76293F65D500E42796 /* PixelDataRecord.swift in Sources */,
@@ -10361,7 +10356,6 @@
3706FDE1293F661700E42796 /* AdjacentItemEnumeratorTests.swift in Sources */,
3706FDE2293F661700E42796 /* PixelArgumentsTests.swift in Sources */,
4B9DB0572A983B55000927DB /* MockNotificationService.swift in Sources */,
- 3706FDE3293F661700E42796 /* History.xcdatamodeld in Sources */,
3706FDE4293F661700E42796 /* TabLazyLoaderTests.swift in Sources */,
3706FDE5293F661700E42796 /* URLEventHandlerTests.swift in Sources */,
3706FDE6293F661700E42796 /* BookmarkOutlineViewDataSourceTests.swift in Sources */,
@@ -10607,6 +10601,7 @@
B62A234129C41D4400D22475 /* HistoryIntegrationTests.swift in Sources */,
B603973929BF0EBE00902A34 /* PrivacyDashboardIntegrationTests.swift in Sources */,
B644B43E29D5682B003FA9AB /* SearchNonexistentDomainTests.swift in Sources */,
+ B6EEDD7E2B8C69E900637EBC /* TabContentTests.swift in Sources */,
3706FEA5293F662100E42796 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */,
B603973D29BF1D7D00902A34 /* AutoconsentIntegrationTests.swift in Sources */,
B60C6F8729B1CAB2007BFAA8 /* TestRunHelper.swift in Sources */,
@@ -10649,6 +10644,7 @@
4B1AD92125FC474E00261379 /* CoreDataEncryptionTesting.xcdatamodeld in Sources */,
B62A234029C41D4400D22475 /* HistoryIntegrationTests.swift in Sources */,
B603973829BF0EBE00902A34 /* PrivacyDashboardIntegrationTests.swift in Sources */,
+ B6EEDD7D2B8C69E900637EBC /* TabContentTests.swift in Sources */,
B644B43D29D56829003FA9AB /* SearchNonexistentDomainTests.swift in Sources */,
B603973C29BF1D7D00902A34 /* AutoconsentIntegrationTests.swift in Sources */,
B60C6F8629B1CAB0007BFAA8 /* TestRunHelper.swift in Sources */,
@@ -11019,6 +11015,7 @@
4B957A192AC7AE700062CA31 /* PasswordManagerCoordinator.swift in Sources */,
4B957A1A2AC7AE700062CA31 /* PasswordManagementIdentityModel.swift in Sources */,
4B957A1B2AC7AE700062CA31 /* UserDefaultsWrapper.swift in Sources */,
+ B65C7DFD2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */,
4B957A1C2AC7AE700062CA31 /* PasswordManagementPopover.swift in Sources */,
4B957A1D2AC7AE700062CA31 /* BWCommunicator.swift in Sources */,
4B957A1E2AC7AE700062CA31 /* HomePageRecentlyVisitedModel.swift in Sources */,
@@ -11028,6 +11025,7 @@
4B957A222AC7AE700062CA31 /* FirefoxBookmarksReader.swift in Sources */,
4B0526622B1D55320054955A /* VPNFeedbackSender.swift in Sources */,
4B957A232AC7AE700062CA31 /* DeviceIdleStateDetector.swift in Sources */,
+ 85D0327D2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */,
4B957A242AC7AE700062CA31 /* FlatButton.swift in Sources */,
4B957A252AC7AE700062CA31 /* PinnedTabView.swift in Sources */,
4B957A262AC7AE700062CA31 /* DataEncryption.swift in Sources */,
@@ -11098,7 +11096,6 @@
4B957A602AC7AE700062CA31 /* NSNotificationName+Debug.swift in Sources */,
4B957A612AC7AE700062CA31 /* NavigationBarBadgeAnimationView.swift in Sources */,
4B957A622AC7AE700062CA31 /* AddressBarButton.swift in Sources */,
- 4B957A632AC7AE700062CA31 /* HistoryEntry.swift in Sources */,
4B957A642AC7AE700062CA31 /* FaviconStore.swift in Sources */,
4B957A652AC7AE700062CA31 /* WaitlistTermsAndConditionsView.swift in Sources */,
B62B48592ADE730D000DECE5 /* FileImportView.swift in Sources */,
@@ -11220,7 +11217,7 @@
4B957ACE2AC7AE700062CA31 /* BrowserTabViewController.swift in Sources */,
4B957ACF2AC7AE700062CA31 /* CallToAction.swift in Sources */,
4B957AD02AC7AE700062CA31 /* MouseOverView.swift in Sources */,
- 4B957AD12AC7AE700062CA31 /* HistoryStore.swift in Sources */,
+ 4B957AD12AC7AE700062CA31 /* EncryptedHistoryStore.swift in Sources */,
4B957AD22AC7AE700062CA31 /* FirePopoverCollectionViewItem.swift in Sources */,
4B957AD32AC7AE700062CA31 /* ArrayExtension.swift in Sources */,
4B957AD42AC7AE700062CA31 /* NetworkProtectionInviteCodeViewModel.swift in Sources */,
@@ -11277,7 +11274,6 @@
4B957B042AC7AE700062CA31 /* UpdateController.swift in Sources */,
4B957B052AC7AE700062CA31 /* FindInPageModel.swift in Sources */,
4B957B062AC7AE700062CA31 /* PseudoFolder.swift in Sources */,
- 4B957B072AC7AE700062CA31 /* Visit.swift in Sources */,
4B2F565D2B38F93E001214C0 /* NetworkProtectionSubscriptionEventHandler.swift in Sources */,
4B957B082AC7AE700062CA31 /* PixelDataStore.swift in Sources */,
4B957B092AC7AE700062CA31 /* WaitlistStorage.swift in Sources */,
@@ -11290,7 +11286,6 @@
B69A14FC2B4D705D00B9417D /* BookmarkFolderPicker.swift in Sources */,
4B957B0F2AC7AE700062CA31 /* FileDownloadError.swift in Sources */,
4B957B102AC7AE700062CA31 /* MoreOrLessView.swift in Sources */,
- 4B957B112AC7AE700062CA31 /* DateExtension.swift in Sources */,
4B957B122AC7AE700062CA31 /* History.xcdatamodeld in Sources */,
4B957B132AC7AE700062CA31 /* PermissionStore.swift in Sources */,
EEC4A6612B277F1100F7C0AA /* NetworkProtectionVPNCountryLabelsModel.swift in Sources */,
@@ -11367,7 +11362,6 @@
4B957B522AC7AE700062CA31 /* NSOpenPanelExtensions.swift in Sources */,
EEC4A66F2B2C894F00F7C0AA /* VPNLocationPreferenceItemModel.swift in Sources */,
4B957B532AC7AE700062CA31 /* FirePopover.swift in Sources */,
- 4B957B542AC7AE700062CA31 /* HistoryCoordinator.swift in Sources */,
4B957B552AC7AE700062CA31 /* NetworkProtectionOnboardingMenu.swift in Sources */,
4B957B562AC7AE700062CA31 /* VariantManager.swift in Sources */,
4B957B572AC7AE700062CA31 /* ApplicationDockMenu.swift in Sources */,
@@ -11446,7 +11440,6 @@
4B957B932AC7AE700062CA31 /* FirePopoverViewModel.swift in Sources */,
4B957B942AC7AE700062CA31 /* BWCommand.swift in Sources */,
4B957B952AC7AE700062CA31 /* NSColorExtension.swift in Sources */,
- 4B957B962AC7AE700062CA31 /* Stored.swift in Sources */,
4B957B972AC7AE700062CA31 /* AddressBarButtonsViewController.swift in Sources */,
4B957B982AC7AE700062CA31 /* BWError.swift in Sources */,
4B957B9A2AC7AE700062CA31 /* PixelDataRecord.swift in Sources */,
@@ -11603,6 +11596,7 @@
B66CA41E2AD910B300447CF0 /* DataImportView.swift in Sources */,
B637273D26CCF0C200C8CB02 /* OptionalExtension.swift in Sources */,
4BE65477271FCD41008D1D63 /* PasswordManagementLoginItemView.swift in Sources */,
+ 85D0327B2B8E3D090041D1FB /* HistoryCoordinatorExtension.swift in Sources */,
AA80EC54256BE3BC007083E7 /* UserText.swift in Sources */,
B61EF3EC266F91E700B4D78F /* WKWebView+Download.swift in Sources */,
311B262728E73E0A00FD181A /* TabShadowConfig.swift in Sources */,
@@ -11635,6 +11629,7 @@
AA6EF9AD25066F42004754E6 /* WindowsManager.swift in Sources */,
1D43EB3A292B63B00065E5D6 /* BWRequest.swift in Sources */,
B684121C2B6A1D880092F66A /* ErrorPageHTMLTemplate.swift in Sources */,
+ B65C7DFB2B886CF0001E2D5C /* WKPDFHUDViewWrapper.swift in Sources */,
B68458CD25C7EB9000DC17B6 /* WKWebViewConfigurationExtensions.swift in Sources */,
85AC7ADD27BEB6EE00FFB69B /* HomePageDefaultBrowserModel.swift in Sources */,
B6619EFB2B111CC500CD9186 /* InstructionsFormatParser.swift in Sources */,
@@ -11879,7 +11874,6 @@
B690152C2ACBF4DA00AD0BAB /* MenuPreview.swift in Sources */,
31F7F2A6288AD2CA001C0D64 /* NavigationBarBadgeAnimationView.swift in Sources */,
AAC5E4F125D6BF10007F5990 /* AddressBarButton.swift in Sources */,
- AAE7527E263B05C600B973F8 /* HistoryEntry.swift in Sources */,
AA5FA69D275F945C00DCE9C9 /* FaviconStore.swift in Sources */,
4B9DB0352A983B24000927DB /* WaitlistTermsAndConditionsView.swift in Sources */,
AAB8203C26B2DE0D00788AC3 /* SuggestionListCharacteristics.swift in Sources */,
@@ -12008,7 +12002,7 @@
AA585D84248FD31100E9A3E2 /* BrowserTabViewController.swift in Sources */,
85707F22276A32B600DC0649 /* CallToAction.swift in Sources */,
B693954B26F04BEB0015B914 /* MouseOverView.swift in Sources */,
- AAE7527C263B056C00B973F8 /* HistoryStore.swift in Sources */,
+ AAE7527C263B056C00B973F8 /* EncryptedHistoryStore.swift in Sources */,
AAE246F32709EF3B00BEEAEE /* FirePopoverCollectionViewItem.swift in Sources */,
4B41EDA32B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */,
AA61C0D22727F59B00E6B681 /* ArrayExtension.swift in Sources */,
@@ -12068,7 +12062,6 @@
85A0118225AF60E700FA6A0C /* FindInPageModel.swift in Sources */,
7BA7CC4E2AD11F6F0042E5CE /* NetworkProtectionIPCTunnelController.swift in Sources */,
4B9292A226670D2A00AD2C21 /* PseudoFolder.swift in Sources */,
- AA7E919A2875B39300AB6B62 /* Visit.swift in Sources */,
4BCF15D92ABB8A7F0083F6DF /* NetworkProtectionRemoteMessage.swift in Sources */,
4B05265E2B1AE5C70054955A /* VPNMetadataCollector.swift in Sources */,
B6DA44022616B28300DD1EC2 /* PixelDataStore.swift in Sources */,
@@ -12080,7 +12073,6 @@
4BE5336C286912D40019DBFD /* BookmarksBarCollectionViewItem.swift in Sources */,
B6C0B23926E742610031CB7F /* FileDownloadError.swift in Sources */,
85589EA027BFE60E0038AD11 /* MoreOrLessView.swift in Sources */,
- B6A9E47026146A250067D1B9 /* DateExtension.swift in Sources */,
AAE7527A263B046100B973F8 /* History.xcdatamodeld in Sources */,
B64C853D26944B940048FEBE /* PermissionStore.swift in Sources */,
AA75A0AE26F3500C0086B667 /* PrivacyIconViewModel.swift in Sources */,
@@ -12160,7 +12152,6 @@
B6DB3AF6278EA0130024C5C4 /* BundleExtension.swift in Sources */,
4B0511E1262CAA8600F6079C /* NSOpenPanelExtensions.swift in Sources */,
AAE99B8927088A19008B6BD9 /* FirePopover.swift in Sources */,
- AAE75280263B0A4D00B973F8 /* HistoryCoordinator.swift in Sources */,
4B0BD7B72A9FE6E500EF609D /* NetworkProtectionOnboardingMenu.swift in Sources */,
B69B503D2726A12500758A2B /* VariantManager.swift in Sources */,
AA97BF4625135DD30014931A /* ApplicationDockMenu.swift in Sources */,
@@ -12227,7 +12218,6 @@
AA13DCB4271480B0006D48D3 /* FirePopoverViewModel.swift in Sources */,
1D43EB38292B636E0065E5D6 /* BWCommand.swift in Sources */,
F41D174125CB131900472416 /* NSColorExtension.swift in Sources */,
- AA7E919C2875C65000AB6B62 /* Stored.swift in Sources */,
AAC5E4F625D6BF2C007F5990 /* AddressBarButtonsViewController.swift in Sources */,
B6F9BDE42B45CD1900677B33 /* ModalView.swift in Sources */,
1D2DC0072901679C008083A1 /* BWError.swift in Sources */,
@@ -12313,7 +12303,6 @@
37D2377C287EBDA300BCE03B /* TabIndexTests.swift in Sources */,
37534CA52811987D002621E7 /* AdjacentItemEnumeratorTests.swift in Sources */,
B6DA44232616CABC00DD1EC2 /* PixelArgumentsTests.swift in Sources */,
- AAEC74BC2642F0F800C2EFBC /* History.xcdatamodeld in Sources */,
56B234BF2A84EFD200F2A1CC /* NavigationBarUrlExtensionsTests.swift in Sources */,
37534C9E28104D9B002621E7 /* TabLazyLoaderTests.swift in Sources */,
B6619EF62B10DFF700CD9186 /* InstructionsFormatParserTests.swift in Sources */,
@@ -13601,7 +13590,7 @@
repositoryURL = "https://github.com/duckduckgo/BrowserServicesKit";
requirement = {
kind = exactVersion;
- version = 113.0.0;
+ version = 114.1.0;
};
};
AA06B6B52672AF8100F541C5 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
@@ -14111,6 +14100,21 @@
isa = XCSwiftPackageProductDependency;
productName = PixelKit;
};
+ 85E2BBCD2B8F534000DBEC7A /* History */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */;
+ productName = History;
+ };
+ 85E2BBCF2B8F534A00DBEC7A /* History */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */;
+ productName = History;
+ };
+ 85E2BBD12B8F536F00DBEC7A /* History */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */;
+ productName = History;
+ };
9807F644278CA16F00E1547B /* BrowserServicesKit */ = {
isa = XCSwiftPackageProductDependency;
package = 9807F643278CA16F00E1547B /* XCRemoteSwiftPackageReference "BrowserServicesKit" */;
diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index b43fb91512..c89d4445e6 100644
--- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -23,8 +23,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/BrowserServicesKit",
"state" : {
- "revision" : "f903ffcbc51e85ac262c355b56726e3387957a80",
- "version" : "113.0.0"
+ "revision" : "045a8782c3dbbf79fc088b38120dea1efadc13e1",
+ "version" : "114.1.0"
}
},
{
diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme
index a37fddc40f..29eecc0cc8 100644
--- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme
+++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser App Store.xcscheme
@@ -1,6 +1,6 @@
-
-
-
-
diff --git a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme
index d9bdde676a..58557db074 100644
--- a/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme
+++ b/DuckDuckGo.xcodeproj/xcshareddata/xcschemes/DuckDuckGo Privacy Browser.xcscheme
@@ -1,6 +1,6 @@
(key: .onboardingFinished, defaultValue: false)
- if !isOnboardingFinished.wrappedValue,
- FileManager.default.fileExists(atPath: URL.sandboxApplicationSupportURL.path) {
- isOnboardingFinished.wrappedValue = true
- }
-
let internalUserDeciderStore = InternalUserDeciderStore(fileStore: fileStore)
internalUserDecider = DefaultInternalUserDecider(store: internalUserDeciderStore)
@@ -212,7 +205,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel
didFinishLaunching = true
}
- HistoryCoordinator.shared.loadHistory()
+ HistoryCoordinator.shared.loadHistory {
+ HistoryCoordinator.shared.migrateModelV5toV6IfNeeded()
+ }
+
PrivacyFeatures.httpsUpgrade.loadDataAsync()
bookmarksManager.loadBookmarks()
if case .normal = NSApp.runType {
@@ -284,7 +280,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, FileDownloadManagerDel
#if SUBSCRIPTION
Task {
- var defaultEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment.default
+ let defaultEnvironment = SubscriptionPurchaseEnvironment.ServiceEnvironment.default
let currentEnvironment = UserDefaultsWrapper(key: .subscriptionEnvironment,
defaultValue: defaultEnvironment).wrappedValue
diff --git a/DuckDuckGo/Bookmarks/Legacy/Bookmark.xcdatamodeld/Bookmark 3.xcdatamodel/contents b/DuckDuckGo/Bookmarks/Legacy/Bookmark.xcdatamodeld/Bookmark 3.xcdatamodel/contents
index b622362964..3572e4db8c 100644
--- a/DuckDuckGo/Bookmarks/Legacy/Bookmark.xcdatamodeld/Bookmark 3.xcdatamodel/contents
+++ b/DuckDuckGo/Bookmarks/Legacy/Bookmark.xcdatamodeld/Bookmark 3.xcdatamodel/contents
@@ -1,13 +1,25 @@
-
+
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/DuckDuckGo/Common/Database/Database.swift b/DuckDuckGo/Common/Database/Database.swift
index e44d63fb33..ad97938c7e 100644
--- a/DuckDuckGo/Common/Database/Database.swift
+++ b/DuckDuckGo/Common/Database/Database.swift
@@ -18,8 +18,8 @@
import AppKit
import BrowserServicesKit
-import Foundation
import CoreData
+import Foundation
import Persistence
final class Database {
@@ -41,17 +41,15 @@ final class Database {
static func makeDatabase() -> (CoreDataDatabase?, Error?) {
func makeDatabase(keyStore: EncryptionKeyStoring, containerLocation: URL) -> (CoreDataDatabase?, Error?) {
- do {
- try EncryptedValueTransformer.registerTransformer(keyStore: keyStore)
- try EncryptedValueTransformer.registerTransformer(keyStore: keyStore)
- try EncryptedValueTransformer.registerTransformer(keyStore: keyStore)
- try EncryptedValueTransformer.registerTransformer(keyStore: keyStore)
- try EncryptedValueTransformer.registerTransformer(keyStore: keyStore)
- try EncryptedValueTransformer.registerTransformer(keyStore: keyStore)
- } catch {
- return (nil, error)
- }
let mainModel = NSManagedObjectModel.mergedModel(from: [.main])!
+ _=mainModel.registerValueTransformers(withAllowedPropertyClasses: [
+ NSImage.self,
+ NSString.self,
+ NSURL.self,
+ NSNumber.self,
+ NSError.self,
+ NSData.self
+ ], keyStore: keyStore)
let httpsUpgradeModel = HTTPSUpgrade.managedObjectModel
return (CoreDataDatabase(name: Constants.databaseName,
@@ -118,6 +116,67 @@ extension Array where Element == CoreDataErrorsParser.ErrorInfo {
}
}
+extension ValueTransformer {
+
+ static func registerValueTransformer(for propertyClass: AnyClass, with keyStore: EncryptionKeyStoring) -> NSValueTransformerName {
+ guard let encodableType = propertyClass as? (NSObject & NSSecureCoding).Type else {
+ fatalError("Unsupported type")
+ }
+ func registerValueTransformer(for type: T.Type) -> NSValueTransformerName {
+ (try? EncryptedValueTransformer.registerTransformer(keyStore: keyStore))!
+ return EncryptedValueTransformer.transformerName
+ }
+ return registerValueTransformer(for: encodableType)
+ }
+
+}
+
+extension NSManagedObjectModel {
+
+ private static let transformerUserInfoKey = "transformer"
+ func registerValueTransformers(withAllowedPropertyClasses allowedPropertyClasses: [AnyClass]? = nil,
+ keyStore: EncryptionKeyStoring) -> [NSValueTransformerName] {
+ var registeredTransformers = [NSValueTransformerName]()
+ let allowedPropertyClassNames = allowedPropertyClasses.map { Set($0.map(NSStringFromClass)) }
+
+ // fix "no NSValueTransformer with class name 'X'" warnings
+ // https://stackoverflow.com/a/77623593/748453
+ for entity in self.entities {
+ for property in entity.properties {
+ guard let property = property as? NSAttributeDescription, property.attributeType == .transformableAttributeType else { continue }
+
+ let transformerName: String
+ if let valueTransformerName = property.valueTransformerName, !valueTransformerName.isEmpty {
+ transformerName = valueTransformerName
+ } else if let transformerUserInfoValue = property.userInfo?[Self.transformerUserInfoKey] as? String, !transformerUserInfoValue.isEmpty {
+ transformerName = transformerUserInfoValue
+ property.userInfo?.removeValue(forKey: Self.transformerUserInfoKey)
+ property.valueTransformerName = transformerName
+ } else {
+ assertionFailure("Transformer (User Info `transformer` key) not set for \(entity).\(property)")
+ continue
+ }
+
+ guard ValueTransformer(forName: .init(rawValue: transformerName)) == nil else { continue }
+
+ let propertyClassName = transformerName.dropping(suffix: "Transformer")
+ assert(propertyClassName != transformerName, "Expected Transformer name like `NSStringTransformer`")
+ guard allowedPropertyClassNames?.contains(propertyClassName) != false,
+ let propertyClass = NSClassFromString(propertyClassName) else {
+ assertionFailure("Invalid class name `\(propertyClassName)` for \(transformerName)")
+ continue
+ }
+
+ let transformer = ValueTransformer.registerValueTransformer(for: propertyClass, with: keyStore)
+ assert(ValueTransformer(forName: .init(transformerName)) != nil)
+ registeredTransformers.append(transformer)
+ }
+ }
+ return registeredTransformers
+ }
+
+}
+
extension NSManagedObjectContext {
func save(onErrorFire event: Pixel.Event.Debug) throws {
diff --git a/DuckDuckGo/Common/Database/Stored.swift b/DuckDuckGo/Common/Database/Stored.swift
deleted file mode 100644
index 69f2c40de1..0000000000
--- a/DuckDuckGo/Common/Database/Stored.swift
+++ /dev/null
@@ -1,30 +0,0 @@
-//
-// Stored.swift
-//
-// Copyright Β© 2022 DuckDuckGo. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Foundation
-
-internal class Stored {
-
- var savingState = SavingState.initialized
-
- enum SavingState {
- case initialized
- case saved
- }
-
-}
diff --git a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift
index 5b39680e13..4705706483 100644
--- a/DuckDuckGo/Common/Extensions/FileManagerExtension.swift
+++ b/DuckDuckGo/Common/Extensions/FileManagerExtension.swift
@@ -21,10 +21,16 @@ import Foundation
extension FileManager {
+ @discardableResult
func moveItem(at srcURL: URL, to destURL: URL, incrementingIndexIfExists flag: Bool, pathExtension: String? = nil) throws -> URL {
return try self.perform(self.moveItem, from: srcURL, to: destURL, incrementingIndexIfExists: flag, pathExtension: pathExtension)
}
+ @discardableResult
+ func copyItem(at srcURL: URL, to destURL: URL, incrementingIndexIfExists flag: Bool, pathExtension: String? = nil) throws -> URL {
+ return try self.perform(self.copyItem, from: srcURL, to: destURL, incrementingIndexIfExists: flag, pathExtension: pathExtension)
+ }
+
private func perform(_ operation: (URL, URL) throws -> Void,
from srcURL: URL,
to destURL: URL,
diff --git a/DuckDuckGo/Common/Extensions/NSBezierPathExtension.swift b/DuckDuckGo/Common/Extensions/NSBezierPathExtension.swift
index 66d3179dc3..e83dc7b9e6 100644
--- a/DuckDuckGo/Common/Extensions/NSBezierPathExtension.swift
+++ b/DuckDuckGo/Common/Extensions/NSBezierPathExtension.swift
@@ -22,25 +22,31 @@ import AppKit
extension NSBezierPath {
- var cgPath: CGPath {
- let path = CGMutablePath()
- var points = [CGPoint](repeating: .zero, count: 3)
- for i in 0 ..< self.elementCount {
- let type = self.element(at: i, associatedPoints: &points)
- switch type {
- case .moveTo:
- path.move(to: points[0])
- case .lineTo:
- path.addLine(to: points[0])
- case .curveTo:
- path.addCurve(to: points[2], control1: points[0], control2: points[1])
- case .closePath:
- path.closeSubpath()
- @unknown default:
- break
+ func asCGPath() -> CGPath {
+ if #available(macOS 14.0, *) {
+ return self.cgPath
+ } else {
+ let path = CGMutablePath()
+ var points = [CGPoint](repeating: .zero, count: 3)
+ for i in 0 ..< self.elementCount {
+ let type = self.element(at: i, associatedPoints: &points)
+ switch type {
+ case .moveTo:
+ path.move(to: points[0])
+ case .lineTo:
+ path.addLine(to: points[0])
+ case .curveTo, .cubicCurveTo:
+ path.addCurve(to: points[2], control1: points[0], control2: points[1])
+ case .quadraticCurveTo:
+ path.addQuadCurve(to: points[1], control: points[0])
+ case .closePath:
+ path.closeSubpath()
+ @unknown default:
+ break
+ }
}
+ return path
}
- return path
}
}
diff --git a/DuckDuckGo/Common/Extensions/NavigationActionExtension.swift b/DuckDuckGo/Common/Extensions/NavigationActionExtension.swift
index f633e99656..41a11f3a84 100644
--- a/DuckDuckGo/Common/Extensions/NavigationActionExtension.swift
+++ b/DuckDuckGo/Common/Extensions/NavigationActionExtension.swift
@@ -43,4 +43,5 @@ extension NavigationAction {
extension CustomNavigationType {
static let userEnteredUrl = CustomNavigationType(rawValue: "userEnteredUrl")
static let tabContentUpdate = CustomNavigationType(rawValue: "tabContentUpdate")
+ static let userRequestedPageDownload = CustomNavigationType(rawValue: "userRequestedPageDownload")
}
diff --git a/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift b/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift
new file mode 100644
index 0000000000..ef4919b1fd
--- /dev/null
+++ b/DuckDuckGo/Common/Extensions/WKPDFHUDViewWrapper.swift
@@ -0,0 +1,97 @@
+//
+// WKPDFHUDViewWrapper.swift
+//
+// Copyright Β© 2024 DuckDuckGo. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import WebKit
+
+/// A wrapper for the PDF HUD window with Zoom controls, Download and Open in Preview buttons
+/// Used to trigger Save PDF
+struct WKPDFHUDViewWrapper {
+
+ static let WKPDFHUDViewClass: AnyClass? = NSClassFromString("WKPDFHUDView")
+ static let performActionForControlSelector = NSSelectorFromString("_performActionForControl:")
+ static let visibleKey = "_visible"
+ static let setVisibleSelector = NSSelectorFromString("_setVisible:")
+ static let savePDFControlId = "arrow.down.circle"
+
+ private let hudView: NSView
+
+ var isVisible: Bool {
+ get {
+ hudView.layer?.sublayers?.first?.opacity ?? 0 > 0
+ }
+ nonmutating set {
+ guard hudView.responds(to: Self.setVisibleSelector) else { return }
+ hudView.perform(Self.setVisibleSelector, with: newValue)
+ }
+ }
+
+ /// Create a wrapper over the PDF HUD view validating its class is `WKPDFHUDView`
+ /// - parameter view: the WKPDFHUDView to wrap
+ /// - returns nil if the view
+ init?(view: NSView) {
+ guard type(of: view) == Self.WKPDFHUDViewClass else { return nil }
+
+ guard Self.WKPDFHUDViewClass?.instancesRespond(to: Self.performActionForControlSelector) == true else {
+ assertionFailure("WKPDFHUDView doesnβt respond to _performActionForControl:")
+ return nil
+ }
+ self.hudView = view
+ }
+
+ /// Find WebViewβs PDF HUD view at a clicked point
+ ///
+ /// Used to get PDF controls view of a clicked WebView frame for `Printβ¦` and `Save Asβ¦` PDF context menu commands
+ static func getPdfHudView(in webView: WKWebView, at location: NSPoint? = nil) -> Self? {
+ guard let hudView = webView.subviews.last(where: { type(of: $0) == Self.WKPDFHUDViewClass && $0.frame.contains(location ?? $0.frame.origin) }) else {
+#if DEBUG
+ Task {
+ if await webView.mimeType == "application/pdf" {
+ assertionFailure("WebView doesnβt have PDF HUD View")
+ }
+ }
+#endif
+ return nil
+ }
+ return self.init(view: hudView)
+ }
+
+ func savePDF() {
+ let wasVisible = isVisible
+ self.setIsVisibleIVar(true)
+ defer {
+ if !wasVisible {
+ self.setIsVisibleIVar(false)
+ }
+ }
+ hudView.perform(Self.performActionForControlSelector, with: Self.savePDFControlId)
+ }
+
+ // try to set _visible ivar value directly to avoid actually showing the HUD
+ private func setIsVisibleIVar(_ value: Bool) {
+ do {
+ try NSException.catch {
+ hudView.setValue(value, forKey: Self.visibleKey)
+ }
+ } catch {
+ assertionFailure("\(error)")
+ self.isVisible = value
+ }
+ }
+
+}
diff --git a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift
index 4c70d4bacc..fcd19de900 100644
--- a/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift
+++ b/DuckDuckGo/Common/Extensions/WKWebViewExtension.swift
@@ -325,6 +325,16 @@ extension WKWebView {
return self.printOperation(with: printInfo)
}
+ func hudView(at point: NSPoint? = nil) -> WKPDFHUDViewWrapper? {
+ WKPDFHUDViewWrapper.getPdfHudView(in: self, at: point)
+ }
+
+ func savePDF(_ pdfHUD: WKPDFHUDViewWrapper? = nil) -> Bool {
+ guard let hudView = pdfHUD ?? hudView() else { return false }
+ hudView.savePDF()
+ return true
+ }
+
var fullScreenPlaceholderView: NSView? {
guard self.responds(to: Selector.fullScreenPlaceholderView) else { return nil }
return self.value(forKey: NSStringFromSelector(Selector.fullScreenPlaceholderView)) as? NSView
diff --git a/DuckDuckGo/Common/FileSystem/WorkspaceProtocol.swift b/DuckDuckGo/Common/FileSystem/WorkspaceProtocol.swift
index ddaf1e337a..3a41f12614 100644
--- a/DuckDuckGo/Common/FileSystem/WorkspaceProtocol.swift
+++ b/DuckDuckGo/Common/FileSystem/WorkspaceProtocol.swift
@@ -22,5 +22,8 @@ protocol Workspace {
func urlForApplication(toOpen url: URL) -> URL?
@discardableResult
func open(_ url: URL) -> Bool
+
+ @discardableResult
+ func open(_ urls: [URL], withAppBundleIdentifier bundleIdentifier: String?, options: NSWorkspace.LaunchOptions, additionalEventParamDescriptor descriptor: NSAppleEventDescriptor?, launchIdentifiers identifiers: AutoreleasingUnsafeMutablePointer?) -> Bool
}
extension NSWorkspace: Workspace {}
diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift
index 7a9cdced0c..47681badff 100644
--- a/DuckDuckGo/Common/Localizables/UserText.swift
+++ b/DuckDuckGo/Common/Localizables/UserText.swift
@@ -375,6 +375,8 @@ struct UserText {
static let downloadsAlwaysAsk = NSLocalizedString("downloads.always-ask", value: "Always ask where to save files", comment: "Downloads preferences checkbox")
static let downloadsChangeDirectory = NSLocalizedString("downloads.change", value: "Changeβ¦", comment: "Change downloads directory button")
+ static let downloadsOpenPopupOnCompletion = NSLocalizedString("downloads.open.on.completion", value: "Automatically open the Downloads panel when downloads complete", comment: "Checkbox to open a Download Manager popover when downloads are completed")
+
// MARK: Password Manager
static let passwordManagement = NSLocalizedString("passsword.management", value: "Autofill", comment: "Used as title for password management user interface")
static let passwordManagementAllItems = NSLocalizedString("passsword.management.all-items", value: "All Items", comment: "Used as title for the Autofill All Items option")
@@ -671,6 +673,8 @@ struct UserText {
static let downloadCanceled = NSLocalizedString("downloads.error.canceled", value: "Canceled", comment: "Short error description when downloaded file download was canceled")
static let downloadFailedToMoveFileToDownloads = NSLocalizedString("downloads.error.move.failed", value: "Could not move file to Downloads", comment: "Short error description when could not move downloaded file to the Downloads folder")
static let downloadFailed = NSLocalizedString("downloads.error.other", value: "Error", comment: "Short error description when Download failed")
+ static let downloadBytesLoadedFormat = NSLocalizedString("downloads.bytes.format", value: "%@ of %@", comment: "Number of bytes out of total bytes downloaded (1Mb of 2Mb)")
+ static let downloadSpeedFormat = NSLocalizedString("downloads.speed.format", value: "%@/s", comment: "Download speed format (1Mb/sec)")
static let cancelDownloadToolTip = NSLocalizedString("downloads.tooltip.cancel", value: "Cancel Download", comment: "Mouse-over tooltip for Cancel Download button")
static let restartDownloadToolTip = NSLocalizedString("downloads.tooltip.restart", value: "Restart Download", comment: "Mouse-over tooltip for Restart Download button")
diff --git a/DuckDuckGo/Common/Logging/Logging.swift b/DuckDuckGo/Common/Logging/Logging.swift
index 25f340d0ae..e6c9d18b36 100644
--- a/DuckDuckGo/Common/Logging/Logging.swift
+++ b/DuckDuckGo/Common/Logging/Logging.swift
@@ -26,7 +26,6 @@ extension OSLog {
case atb = "ATB"
case config = "Configuration Downloading"
case fire = "Fire"
- case history = "History"
case dataImportExport = "Data Import/Export"
case pixel = "Pixel"
case contentBlocking = "Content Blocking"
@@ -55,7 +54,6 @@ extension OSLog {
@OSLogWrapper(.atb) static var atb
@OSLogWrapper(.config) static var config
@OSLogWrapper(.fire) static var fire
- @OSLogWrapper(.history) static var history
@OSLogWrapper(.dataImportExport) static var dataImportExport
@OSLogWrapper(.pixel) static var pixel
@OSLogWrapper(.contentBlocking) static var contentBlocking
diff --git a/DuckDuckGo/Common/Utilities/HardwareModel.swift b/DuckDuckGo/Common/Utilities/HardwareModel.swift
index 5b0ab5a74b..70768376ed 100644
--- a/DuckDuckGo/Common/Utilities/HardwareModel.swift
+++ b/DuckDuckGo/Common/Utilities/HardwareModel.swift
@@ -22,7 +22,15 @@ import IOKit
struct HardwareModel {
static var model: String? {
- let service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"))
+ let port: mach_port_t
+
+ if #available(macOS 12.0, *) {
+ port = kIOMainPortDefault
+ } else {
+ port = kIOMasterPortDefault
+ }
+
+ let service = IOServiceGetMatchingService(port, IOServiceMatching("IOPlatformExpertDevice"))
var modelIdentifier: String?
if let modelData = IORegistryEntryCreateCFProperty(
diff --git a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
index 8ad5fa3359..272d63cca0 100644
--- a/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
+++ b/DuckDuckGo/Common/Utilities/UserDefaultsWrapper.swift
@@ -58,6 +58,7 @@ public struct UserDefaultsWrapper {
case selectedDownloadLocationKey = "preferences.download-location"
case lastUsedCustomDownloadLocation = "preferences.custom-last-used-download-location"
case alwaysRequestDownloadLocationKey = "preferences.download-location.always-request"
+ case openDownloadsPopupOnCompletionKey = "preferences.downloads.open.on.completion"
case autoconsentEnabled = "preferences.autoconsent-enabled"
case duckPlayerMode = "preferences.duck-player"
case youtubeOverlayInteracted = "preferences.youtube-overlay-interacted"
diff --git a/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift b/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift
index bbeac9b5b1..7afb3d8784 100644
--- a/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift
+++ b/DuckDuckGo/Common/View/AppKit/CircularProgressView.swift
@@ -275,7 +275,7 @@ private extension CAShapeLayer {
self.bounds = CGRect(x: 0, y: 0, width: (radius + lineWidth) * 2, height: (radius + lineWidth) * 2)
let rect = NSRect(x: lineWidth, y: lineWidth, width: radius * 2, height: radius * 2)
- self.path = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius).cgPath
+ self.path = NSBezierPath(roundedRect: rect, xRadius: radius, yRadius: radius).asCGPath()
self.lineWidth = lineWidth
self.fillColor = NSColor.clear.cgColor
diff --git a/DuckDuckGo/Common/View/SwiftUI/ViewExtension.swift b/DuckDuckGo/Common/View/SwiftUI/ViewExtension.swift
index 8f5cd6f2dd..fa1682f58b 100644
--- a/DuckDuckGo/Common/View/SwiftUI/ViewExtension.swift
+++ b/DuckDuckGo/Common/View/SwiftUI/ViewExtension.swift
@@ -125,7 +125,7 @@ private struct RoundedCorner: Shape {
func path(in rect: CGRect) -> Path {
let path = NSBezierPath(roundedRect: rect, forCorners: corners, cornerRadius: radius)
- return Path(path.cgPath)
+ return Path(path.asCGPath())
}
}
diff --git a/DuckDuckGo/Favicons/Model/FaviconManager.swift b/DuckDuckGo/Favicons/Model/FaviconManager.swift
index 28203bafb7..87842b8e88 100644
--- a/DuckDuckGo/Favicons/Model/FaviconManager.swift
+++ b/DuckDuckGo/Favicons/Model/FaviconManager.swift
@@ -21,6 +21,7 @@ import Cocoa
import Combine
import BrowserServicesKit
import Common
+import History
@MainActor
protocol FaviconManagement: AnyObject {
diff --git a/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift b/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift
index 9ae1945b25..eb271ab5a6 100644
--- a/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift
+++ b/DuckDuckGo/FileDownload/Model/FileDownloadManager.swift
@@ -83,10 +83,11 @@ final class FileDownloadManager: FileDownloadManagerProtocol {
return url
}
- var promptForLocation: Bool {
+ func shouldPromptForLocation(for url: URL?) -> Bool {
switch self {
case .prompt: return true
- case .preset, .auto: return false
+ case .preset: return false
+ case .auto: return url?.isFileURL ?? true // always prompt when "downloading" a local file
}
}
}
@@ -96,7 +97,7 @@ final class FileDownloadManager: FileDownloadManagerProtocol {
dispatchPrecondition(condition: .onQueue(.main))
let task = WebKitDownloadTask(download: download,
- promptForLocation: location.promptForLocation,
+ promptForLocation: location.shouldPromptForLocation(for: download.originalRequest?.url),
destinationURL: location.destinationURL,
tempURL: location.tempURL,
isBurner: fromBurnerWindow)
diff --git a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift
index 4e96b1f482..a69b84e30d 100644
--- a/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift
+++ b/DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift
@@ -16,9 +16,9 @@
// limitations under the License.
//
-import Navigation
import Combine
import Foundation
+import Navigation
import UniformTypeIdentifiers
import WebKit
@@ -33,6 +33,10 @@ protocol WebKitDownloadTaskDelegate: AnyObject {
final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable {
static let downloadExtension = "duckload"
+ private enum Constants {
+ static let remainingDownloadTimeEstimationDelay: TimeInterval = 1
+ static let downloadSpeedSmoothingFactor = 0.1
+ }
let progress: Progress
let shouldPromptForLocation: Bool
@@ -72,7 +76,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable
private weak var delegate: WebKitDownloadTaskDelegate?
private let download: WebKitDownload
- private var cancellables = Set()
+ private var progressCancellable: AnyCancellable?
private var decideDestinationCompletionHandler: ((URL?) -> Void)?
@@ -114,12 +118,37 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable
private func start() {
self.progress.fileDownloadingSourceURL = download.originalRequest?.url
if let progress = (self.download as? ProgressReporting)?.progress {
- progress.publisher(for: \.totalUnitCount)
- .assign(to: \.totalUnitCount, onWeaklyHeld: self.progress)
- .store(in: &self.cancellables)
- progress.publisher(for: \.completedUnitCount)
- .assign(to: \.completedUnitCount, onWeaklyHeld: self.progress)
- .store(in: &self.cancellables)
+
+ var startTime: Date?
+ progressCancellable = progress.publisher(for: \.totalUnitCount)
+ .combineLatest(progress.publisher(for: \.completedUnitCount))
+ .sink { [weak progress=self.progress] total, completed in
+ guard let progress else { return }
+ if progress.totalUnitCount != total {
+ progress.totalUnitCount = total
+ }
+ progress.completedUnitCount = completed
+
+ if total > 0, completed > 0 {
+ guard let startTime else {
+ startTime = Date()
+ return
+ }
+ let elapsedTime = Date().timeIntervalSince(startTime)
+ // delay before we start calculating the estimated time - because initially itβs not reliable
+ guard elapsedTime > Constants.remainingDownloadTimeEstimationDelay else { return }
+
+ // calculate instantaneous download speed
+ var throughput = Double(completed) / elapsedTime
+
+ // calculate the moving average of download speed
+ if let oldThroughput = progress.throughput.map(Double.init) {
+ throughput = Constants.downloadSpeedSmoothingFactor * throughput + (1 - Constants.downloadSpeedSmoothingFactor) * oldThroughput
+ }
+ progress.throughput = Int(throughput)
+ progress.estimatedTimeRemaining = Double(total - completed) / throughput
+ }
+ }
}
}
@@ -269,7 +298,7 @@ extension WebKitDownloadTask: WKDownloadDelegate {
// sometimes suggesteFilename has an extension appended to already present URL file extension
// e.g. feed.xml.rss for www.domain.com/rss.xml
if let urlSuggestedFilename = response.url?.suggestedFilename,
- !urlSuggestedFilename.pathExtension.isEmpty,
+ !(urlSuggestedFilename.pathExtension.isEmpty || (self.suggestedFileType == .html && urlSuggestedFilename.pathExtension == "html")),
suggestedFilename.hasPrefix(urlSuggestedFilename) {
suggestedFilename = urlSuggestedFilename
}
diff --git a/DuckDuckGo/FileDownload/View/Downloads.storyboard b/DuckDuckGo/FileDownload/View/Downloads.storyboard
index 411575e7a9..41e5511da3 100644
--- a/DuckDuckGo/FileDownload/View/Downloads.storyboard
+++ b/DuckDuckGo/FileDownload/View/Downloads.storyboard
@@ -11,14 +11,14 @@
-
+
-
+
-
+
@@ -29,7 +29,7 @@
-
+
@@ -58,7 +58,7 @@
-
+
@@ -87,18 +87,18 @@
-
+
-
+
-
+
-
+
@@ -111,7 +111,7 @@
-
+
@@ -123,7 +123,7 @@
-
+
@@ -134,7 +134,7 @@
-
+
@@ -142,7 +142,7 @@
-
+
@@ -170,7 +170,7 @@
-
+
@@ -198,7 +198,7 @@
-
+
@@ -226,14 +226,14 @@
-
+
-
+
@@ -247,12 +247,12 @@
-
+
-
+
@@ -270,11 +270,11 @@
-
+
-
+
@@ -282,7 +282,7 @@
-
+
@@ -305,11 +305,11 @@
-
+
-
+
@@ -356,7 +356,6 @@
-
diff --git a/DuckDuckGo/FileDownload/View/DownloadsCellView.swift b/DuckDuckGo/FileDownload/View/DownloadsCellView.swift
index b891bbb620..662a085cd1 100644
--- a/DuckDuckGo/FileDownload/View/DownloadsCellView.swift
+++ b/DuckDuckGo/FileDownload/View/DownloadsCellView.swift
@@ -53,7 +53,31 @@ final class DownloadsCellView: NSTableCellView {
private var cancellables = Set()
private var progressCancellable: AnyCancellable?
- private static let byteFormatter = ByteCountFormatter()
+ private static let byteFormatter: ByteCountFormatter = {
+ let formatter = ByteCountFormatter()
+ formatter.isAdaptive = true
+ formatter.allowsNonnumericFormatting = false
+ formatter.zeroPadsFractionDigits = true
+ return formatter
+ }()
+
+ private static let estimatedMinutesRemainingFormatter: DateComponentsFormatter = {
+ let formatter = DateComponentsFormatter()
+ formatter.allowedUnits = [.hour, .minute]
+ formatter.unitsStyle = .brief
+ formatter.includesApproximationPhrase = false
+ formatter.includesTimeRemainingPhrase = true
+ return formatter
+ }()
+
+ private static let estimatedSecondsRemainingFormatter: DateComponentsFormatter = {
+ let formatter = DateComponentsFormatter()
+ formatter.allowedUnits = [.second]
+ formatter.unitsStyle = .brief
+ formatter.includesApproximationPhrase = false
+ formatter.includesTimeRemainingPhrase = true
+ return formatter
+ }()
var isSelected: Bool = false {
didSet {
@@ -139,35 +163,56 @@ final class DownloadsCellView: NSTableCellView {
private var onButtonMouseOverChange: ((Bool) -> Void)?
- private func updateDetails(with progress: Progress) {
+ private func updateDetails(with progress: Progress, isMouseOver: Bool) {
+ self.detailLabel.toolTip = nil
+
var details: String
- if cancelButton.isMouseOver {
+ var estimatedTime: String = ""
+ if isMouseOver {
details = UserText.cancelDownloadToolTip
} else {
- details = progress.localizedAdditionalDescription ?? ""
- if details.isEmpty {
- if progress.fractionCompleted == 0 {
- details = UserText.downloadStarting
- } else if progress.fractionCompleted == 1.0 {
- details = UserText.downloadFinishing
- } else {
- assertionFailure("Unexpected empty description")
- details = "Downloadingβ¦"
+ if progress.fractionCompleted == 0 {
+ details = UserText.downloadStarting
+ } else if progress.fractionCompleted == 1.0 {
+ details = UserText.downloadFinishing
+ } else if progress.totalUnitCount > 0 {
+ let completed = Self.byteFormatter.string(fromByteCount: progress.completedUnitCount)
+ let total = Self.byteFormatter.string(fromByteCount: progress.totalUnitCount)
+ details = String(format: UserText.downloadBytesLoadedFormat, completed, total)
+
+ if let throughput = progress.throughput {
+ let speed = Self.byteFormatter.string(fromByteCount: Int64(throughput))
+ details += " (\(String(format: UserText.downloadSpeedFormat, speed)))"
}
+ } else {
+ details = Self.byteFormatter.string(fromByteCount: progress.completedUnitCount)
}
- self.detailLabel.toolTip = progress.localizedDescription
+ if let estimatedTimeRemaining = progress.estimatedTimeRemaining,
+ // only set estimated time if already present or more than 10 seconds remaining to avoid blinking
+ !self.detailLabel.stringValue.contains("β") || estimatedTimeRemaining > 10,
+ let estimatedTimeStr = {
+ switch estimatedTimeRemaining {
+ case ..<60:
+ Self.estimatedSecondsRemainingFormatter.string(from: estimatedTimeRemaining)
+ default:
+ Self.estimatedMinutesRemainingFormatter.string(from: estimatedTimeRemaining)
+ }
+ }() {
+ estimatedTime = estimatedTimeStr
+ }
}
- self.detailLabel.stringValue = details
+ self.detailLabel.stringValue = details + (estimatedTime.isEmpty ? "" : " β " + estimatedTime)
}
private func subscribe(to progress: Progress) {
self.progressView.isHidden = false
progressCancellable = progress.publisher(for: \.completedUnitCount)
- .throttle(for: 0.2, scheduler: DispatchQueue.main, latest: true)
+ .throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] _ in
- self?.updateDetails(with: progress)
+ guard let self else { return }
+ updateDetails(with: progress, isMouseOver: cancelButton.isMouseOver)
}
self.cancelButton.isHidden = false
@@ -176,8 +221,8 @@ final class DownloadsCellView: NSTableCellView {
self.imageView?.alphaValue = 1.0
- onButtonMouseOverChange = { [weak self] _ in
- self?.updateDetails(with: progress)
+ onButtonMouseOverChange = { [weak self] isMouseOver in
+ self?.updateDetails(with: progress, isMouseOver: isMouseOver)
}
onButtonMouseOverChange!(cancelButton.isMouseOver)
}
diff --git a/DuckDuckGo/FileDownload/View/DownloadsViewController.swift b/DuckDuckGo/FileDownload/View/DownloadsViewController.swift
index 21c286ce61..8500098dbc 100644
--- a/DuckDuckGo/FileDownload/View/DownloadsViewController.swift
+++ b/DuckDuckGo/FileDownload/View/DownloadsViewController.swift
@@ -27,6 +27,8 @@ protocol DownloadsViewControllerDelegate: AnyObject {
final class DownloadsViewController: NSViewController {
+ static let preferredContentSize = CGSize(width: 420, height: 500)
+
static func create() -> Self {
let storyboard = NSStoryboard(name: "Downloads", bundle: nil)
// swiftlint:disable force_cast
@@ -65,8 +67,11 @@ final class DownloadsViewController: NSViewController {
setupDragAndDrop()
setUpStrings()
+
openDownloadsFolderButton.toolTip = UserText.openDownloadsFolderTooltip
clearDownloadsButton.toolTip = UserText.clearDownloadHistoryTooltip
+
+ preferredContentSize = Self.preferredContentSize
}
override func viewWillAppear() {
@@ -142,13 +147,36 @@ final class DownloadsViewController: NSViewController {
// MARK: User Actions
@IBAction func openDownloadsFolderAction(_ sender: Any) {
- guard let url = DownloadsPreferences().effectiveDownloadLocation
- ?? FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
- else {
- return
+ let prefs = DownloadsPreferences()
+ var url: URL?
+ var itemToSelect: URL?
+
+ if prefs.alwaysRequestDownloadLocation {
+ url = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
+
+ if let lastDownloaded = viewModel.items.first/* last added */(where: {
+ // should still exist
+ $0.localURL != nil && FileManager.default.fileExists(atPath: $0.localURL!.deletingLastPathComponent().path)
+ }),
+ let lastDownloadedURL = lastDownloaded.localURL,
+ // if no downloads are from the default Downloads folder - open the last downloaded item folder
+ !viewModel.items.contains(where: { $0.localURL?.deletingLastPathComponent().path == url?.path }) || url == nil {
+
+ url = lastDownloadedURL.deletingLastPathComponent()
+ // select last downloaded item
+ itemToSelect = lastDownloadedURL
+
+ } /* else fallback to default Userβs Downloads */
+
+ } else {
+ // open preferred downlod location
+ url = prefs.effectiveDownloadLocation ?? FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first
}
+
+ guard let url else { return }
+
self.dismiss()
- NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: url.path)
+ NSWorkspace.shared.selectFile(itemToSelect?.path, inFileViewerRootedAtPath: url.path)
}
@IBAction func clearDownloadsAction(_ sender: Any) {
diff --git a/DuckDuckGo/Fire/Model/Fire.swift b/DuckDuckGo/Fire/Model/Fire.swift
index f67d4072e4..29c2ca70f0 100644
--- a/DuckDuckGo/Fire/Model/Fire.swift
+++ b/DuckDuckGo/Fire/Model/Fire.swift
@@ -23,6 +23,7 @@ import DDGSync
import PrivacyDashboard
import WebKit
import SecureStorage
+import History
final class Fire {
diff --git a/DuckDuckGo/Fire/View/FirePopoverViewController.swift b/DuckDuckGo/Fire/View/FirePopoverViewController.swift
index abf613a442..99376f1039 100644
--- a/DuckDuckGo/Fire/View/FirePopoverViewController.swift
+++ b/DuckDuckGo/Fire/View/FirePopoverViewController.swift
@@ -19,6 +19,7 @@
import Cocoa
import Combine
import Common
+import History
protocol FirePopoverViewControllerDelegate: AnyObject {
diff --git a/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift b/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift
index c9209c528a..3ed92a3ec1 100644
--- a/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift
+++ b/DuckDuckGo/Fire/ViewModel/FirePopoverViewModel.swift
@@ -19,6 +19,7 @@
import Cocoa
import BrowserServicesKit
import Common
+import History
@MainActor
final class FirePopoverViewModel {
diff --git a/DuckDuckGo/History/Model/History.swift b/DuckDuckGo/History/Model/History.swift
deleted file mode 100644
index 6ae4eeded0..0000000000
--- a/DuckDuckGo/History/Model/History.swift
+++ /dev/null
@@ -1,21 +0,0 @@
-//
-// History.swift
-//
-// Copyright Β© 2021 DuckDuckGo. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Foundation
-
-typealias History = Set
diff --git a/DuckDuckGo/History/Model/HistoryCoordinator.swift b/DuckDuckGo/History/Model/HistoryCoordinator.swift
deleted file mode 100644
index 9954e13af1..0000000000
--- a/DuckDuckGo/History/Model/HistoryCoordinator.swift
+++ /dev/null
@@ -1,375 +0,0 @@
-//
-// HistoryCoordinator.swift
-//
-// Copyright Β© 2021 DuckDuckGo. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Foundation
-import Combine
-import Common
-import BrowserServicesKit
-
-typealias History = [HistoryEntry]
-
-protocol HistoryCoordinating: AnyObject {
-
- var history: History? { get }
- var allHistoryVisits: [Visit]? { get }
- var historyDictionaryPublisher: Published<[URL: HistoryEntry]?>.Publisher { get }
-
- func addVisit(of url: URL) -> Visit?
- func addBlockedTracker(entityName: String, on url: URL)
- func trackerFound(on: URL)
- func updateTitleIfNeeded(title: String, url: URL)
- func markFailedToLoadUrl(_ url: URL)
- func commitChanges(url: URL)
-
- func title(for url: URL) -> String?
-
- func burnAll(completion: @escaping () -> Void)
- func burnDomains(_ baseDomains: Set, tld: TLD, completion: @escaping () -> Void)
- func burnVisits(_ visits: [Visit], completion: @escaping () -> Void)
-
-}
-
-/// Coordinates access to History. Uses its own queue with high qos for all operations.
-final class HistoryCoordinator: HistoryCoordinating {
- static let shared = HistoryCoordinator()
-
- init() {}
-
- init(historyStoring: HistoryStoring) {
- self.historyStoring = historyStoring
- historyDictionary = [:]
- }
-
- func loadHistory() {
- cleanOldAndLoad { [weak self] _ in
- self?.migrateModelV5toV6IfNeeded()
- }
- scheduleRegularCleaning()
- }
-
- private lazy var historyStoring: HistoryStoring = HistoryStore()
- private var regularCleaningTimer: Timer?
-
- // Source of truth
- @Published private(set) var historyDictionary: [URL: HistoryEntry]?
- var historyDictionaryPublisher: Published<[URL: HistoryEntry]?>.Publisher { $historyDictionary }
-
- // Output
- var history: History? {
- guard let historyDictionary = historyDictionary else {
- return nil
- }
-
- return makeHistory(from: historyDictionary)
- }
-
- var allHistoryVisits: [Visit]? {
- history?.flatMap { $0.visits }
- }
-
- private var cancellables = Set()
-
- @discardableResult func addVisit(of url: URL) -> Visit? {
- guard let historyDictionary = historyDictionary else {
- os_log("Visit of %s ignored", log: .history, url.absoluteString)
- return nil
- }
-
- let entry = historyDictionary[url] ?? HistoryEntry(url: url)
- let visit = entry.addVisit()
- entry.failedToLoad = false
-
- self.historyDictionary?[url] = entry
-
- commitChanges(url: url)
- return visit
- }
-
- func addBlockedTracker(entityName: String, on url: URL) {
- guard let historyDictionary = historyDictionary else {
- os_log("Add tracker to %s ignored, no history", log: .history, url.absoluteString)
- return
- }
-
- guard let entry = historyDictionary[url] else {
- os_log("Add tracker to %s ignored, no entry", log: .history, url.absoluteString)
- return
- }
-
- entry.addBlockedTracker(entityName: entityName)
- }
-
- func trackerFound(on url: URL) {
- guard let historyDictionary = historyDictionary else {
- os_log("Add tracker to %s ignored, no history", log: .history, url.absoluteString)
- return
- }
-
- guard let entry = historyDictionary[url] else {
- os_log("Add tracker to %s ignored, no entry", log: .history, url.absoluteString)
- return
- }
-
- entry.trackersFound = true
- }
-
- func updateTitleIfNeeded(title: String, url: URL) {
- guard let historyDictionary = historyDictionary else { return }
- guard let entry = historyDictionary[url] else {
- os_log("Title update ignored - URL not part of history yet", log: .history, type: .debug)
- return
- }
- guard !title.isEmpty, entry.title != title else { return }
-
- entry.title = title
- }
-
- func markFailedToLoadUrl(_ url: URL) {
- mark(url: url, keyPath: \HistoryEntry.failedToLoad, value: true)
- }
-
- func commitChanges(url: URL) {
- guard let historyDictionary = historyDictionary,
- let entry = historyDictionary[url] else {
- return
- }
-
- save(entry: entry)
- }
-
- func title(for url: URL) -> String? {
- guard let historyEntry = historyDictionary?[url] else {
- return nil
- }
-
- return historyEntry.title
- }
-
- func burnAll(completion: @escaping () -> Void) {
- guard let historyDictionary = historyDictionary else { return }
-
- let entries = Array(historyDictionary.values)
-
- removeEntries(entries, completionHandler: { _ in
- completion()
- })
- }
-
- func burnDomains(_ baseDomains: Set, tld: TLD, completion: @escaping () -> Void) {
- guard let historyDictionary = historyDictionary else { return }
-
- let entries: [HistoryEntry] = historyDictionary.values.filter { historyEntry in
- guard let host = historyEntry.url.host, let baseDomain = tld.eTLDplus1(host) else {
- return false
- }
-
- return baseDomains.contains(baseDomain)
- }
-
- removeEntries(entries, completionHandler: { _ in
- completion()
- })
- }
-
- func burnVisits(_ visits: [Visit], completion: @escaping () -> Void) {
- removeVisits(visits) { _ in
- completion()
- }
- }
-
- var cleaningDate: Date { .monthAgo }
-
- @objc private func cleanOld() {
- clean(until: cleaningDate)
- }
-
- private func cleanOldAndLoad(completionHandler: ((Error?) -> Void)?) {
- clean(until: cleaningDate, completionHandler: completionHandler)
- }
-
- private func clean(until date: Date,
- completionHandler: ((Error?) -> Void)? = nil) {
- historyStoring.cleanOld(until: date)
- .receive(on: DispatchQueue.main)
- .sink(receiveCompletion: { completion in
- switch completion {
- case .finished:
- os_log("History cleaned successfully", log: .history)
- completionHandler?(nil)
- case .failure(let error):
- os_log("Cleaning of history failed: %s", log: .history, type: .error, error.localizedDescription)
- completionHandler?(error)
- }
- }, receiveValue: { [weak self] history in
- self?.historyDictionary = self?.makeHistoryDictionary(from: history)
- })
- .store(in: &cancellables)
- }
-
- private func removeEntries(_ entries: [HistoryEntry],
- completionHandler: ((Error?) -> Void)? = nil) {
- // Remove from the local memory
- entries.forEach { entry in
- historyDictionary?.removeValue(forKey: entry.url)
- }
-
- // Remove from the storage
- historyStoring.removeEntries(entries)
- .receive(on: DispatchQueue.main)
- .sink(receiveCompletion: { completion in
- switch completion {
- case .finished:
- os_log("Entries removed successfully", log: .history)
- completionHandler?(nil)
- case .failure(let error):
- assertionFailure("Removal failed")
- os_log("Removal failed: %s", log: .history, type: .error, error.localizedDescription)
- completionHandler?(error)
- }
- }, receiveValue: {})
- .store(in: &cancellables)
- }
-
- private func removeVisits(_ visits: [Visit],
- completionHandler: ((Error?) -> Void)? = nil) {
- var entriesToRemove = [HistoryEntry]()
-
- // Remove from the local memory
- visits.forEach { visit in
- if let historyEntry = visit.historyEntry {
- historyEntry.visits.remove(visit)
-
- if historyEntry.visits.count > 0 {
- if let newLastVisit = historyEntry.visits.map({ $0.date }).max() {
- historyEntry.lastVisit = newLastVisit
- save(entry: historyEntry)
- } else {
- assertionFailure("No history entry")
- }
- } else {
- entriesToRemove.append(historyEntry)
- }
- } else {
- assertionFailure("No history entry")
- }
- }
-
- // Remove from the storage
- historyStoring.removeVisits(visits)
- .receive(on: DispatchQueue.main)
- .sink(receiveCompletion: { [weak self] completion in
- switch completion {
- case .finished:
- os_log("Visits removed successfully", log: .history)
- // Remove entries with no remaining visits
- self?.removeEntries(entriesToRemove, completionHandler: completionHandler)
- case .failure(let error):
- assertionFailure("Removal failed")
- os_log("Removal failed: %s", log: .history, type: .error, error.localizedDescription)
- completionHandler?(error)
- }
- }, receiveValue: {})
- .store(in: &cancellables)
- }
-
- private func scheduleRegularCleaning() {
- let timer = Timer(fireAt: .startOfDayTomorrow,
- interval: .day,
- target: self,
- selector: #selector(cleanOld),
- userInfo: nil,
- repeats: true)
- RunLoop.main.add(timer, forMode: RunLoop.Mode.common)
- regularCleaningTimer = timer
- }
-
- private func makeHistoryDictionary(from history: History) -> [URL: HistoryEntry] {
- dispatchPrecondition(condition: .onQueue(.main))
-
- return history.reduce(into: [URL: HistoryEntry](), { $0[$1.url] = $1 })
- }
-
- private func makeHistory(from dictionary: [URL: HistoryEntry]) -> History {
- dispatchPrecondition(condition: .onQueue(.main))
-
- return History(dictionary.values)
- }
-
- private func save(entry: HistoryEntry) {
- guard let entryCopy = entry.copy() as? HistoryEntry else {
- assertionFailure("Copying HistoryEntry failed")
- return
- }
- entry.visits.forEach { $0.savingState = .saved }
-
- historyStoring.save(entry: entryCopy)
- .receive(on: DispatchQueue.main)
- .sink(receiveCompletion: { completion in
- switch completion {
- case .finished:
- os_log("Visit entry updated successfully. URL: %s, Title: %s, Number of visits: %d, failed to load: %s",
- log: .history,
- entry.url.absoluteString,
- entry.title ?? "",
- entry.numberOfTotalVisits,
- entry.failedToLoad ? "yes" : "no")
- case .failure(let error):
- os_log("Saving of history entry failed: %s", log: .history, type: .error, error.localizedDescription)
- }
- }, receiveValue: { result in
- for (id, date) in result {
- if let visit = entry.visits.first(where: { $0.date == date }) {
- visit.identifier = id
- }
- }
- })
- .store(in: &cancellables)
- }
-
- /// Sets boolean value for the keyPath in HistroryEntry for the specified url
- /// Does the same for the root URL if it has no visits
- private func mark(url: URL, keyPath: WritableKeyPath, value: Bool) {
- guard let historyDictionary = historyDictionary, var entry = historyDictionary[url] else {
- os_log("Marking of %s not saved. History not loaded yet or entry doesn't exist",
- log: .history, url.absoluteString)
- return
- }
-
- entry[keyPath: keyPath] = value
- }
-
- // V5 to V6 custom migration
-
- @UserDefaultsWrapper(key: .historyV5toV6Migration, defaultValue: false)
- private var historyV5toV6Migration: Bool
-
- private func migrateModelV5toV6IfNeeded() {
- guard let historyDictionary = historyDictionary,
- !historyV5toV6Migration else {
- return
- }
-
- historyV5toV6Migration = true
-
- for entry in historyDictionary.values where entry.visits.isEmpty {
- entry.addOldVisit(date: entry.lastVisit)
- save(entry: entry)
- }
- }
-
-}
diff --git a/DuckDuckGo/History/Model/HistoryEntry.swift b/DuckDuckGo/History/Model/HistoryEntry.swift
deleted file mode 100644
index a3c3dc78b6..0000000000
--- a/DuckDuckGo/History/Model/HistoryEntry.swift
+++ /dev/null
@@ -1,142 +0,0 @@
-//
-// HistoryEntry.swift
-//
-// Copyright Β© 2021 DuckDuckGo. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Foundation
-import BrowserServicesKit
-
-final class HistoryEntry {
-
- init(identifier: UUID,
- url: URL,
- title: String? = nil,
- failedToLoad: Bool,
- numberOfTotalVisits: Int,
- lastVisit: Date,
- visits: Set,
- numberOfTrackersBlocked: Int,
- blockedTrackingEntities: Set,
- trackersFound: Bool) {
- self.identifier = identifier
- self.url = url
- self.title = title
- self.failedToLoad = failedToLoad
- self.numberOfTotalVisits = numberOfTotalVisits
- self.lastVisit = lastVisit
- self.visits = visits
- self.numberOfTrackersBlocked = numberOfTrackersBlocked
- self.blockedTrackingEntities = blockedTrackingEntities
- self.trackersFound = trackersFound
- }
-
- let identifier: UUID
- let url: URL
- var title: String?
- var failedToLoad: Bool
-
- // MARK: - Visits
-
- // Kept here because of migration. Can be used as computed property once visits of HistoryEntryMO are filled with all necessary info
- // (In use for 1 month by majority of users)
- var numberOfTotalVisits: Int
- var lastVisit: Date
-
- var visits: Set
-
- func addVisit() -> Visit {
- let visit = Visit(date: Date(), historyEntry: self)
- visits.insert(visit)
-
- numberOfTotalVisits += 1
- lastVisit = Date()
-
- return visit
- }
-
- // Used for migration
- func addOldVisit(date: Date) {
- let visit = Visit(date: date, historyEntry: self)
- visits.insert(visit)
- }
-
- // MARK: - Tracker blocking info
-
- var numberOfTrackersBlocked: Int
- var blockedTrackingEntities: Set
- var trackersFound: Bool
-
- func addBlockedTracker(entityName: String) {
- numberOfTrackersBlocked += 1
-
- guard !entityName.trimmingWhitespace().isEmpty else {
- return
- }
- blockedTrackingEntities.insert(entityName)
- }
-
-}
-
-extension HistoryEntry {
-
- convenience init(url: URL) {
- self.init(identifier: UUID(),
- url: url,
- title: nil,
- failedToLoad: false,
- numberOfTotalVisits: 0,
- lastVisit: Date.startOfMinuteNow,
- visits: Set(),
- numberOfTrackersBlocked: 0,
- blockedTrackingEntities: Set(),
- trackersFound: false)
- }
-
-}
-
-extension HistoryEntry: Hashable {
-
- static func == (lhs: HistoryEntry, rhs: HistoryEntry) -> Bool {
- lhs === rhs
- }
-
- func hash(into hasher: inout Hasher) {
- hasher.combine(id)
- }
-
-}
-
-extension HistoryEntry: Identifiable {}
-
-extension HistoryEntry: NSCopying {
-
- func copy(with zone: NSZone? = nil) -> Any {
- let visits = visits.compactMap { $0.copy() as? Visit }
- let entry = HistoryEntry(identifier: identifier,
- url: url,
- title: title,
- failedToLoad: failedToLoad,
- numberOfTotalVisits: numberOfTotalVisits,
- lastVisit: lastVisit,
- visits: Set(visits),
- numberOfTrackersBlocked: numberOfTrackersBlocked,
- blockedTrackingEntities: blockedTrackingEntities,
- trackersFound: trackersFound)
- entry.visits.forEach { $0.historyEntry = entry }
- return entry
- }
-
-}
diff --git a/DuckDuckGo/History/Model/Visit.swift b/DuckDuckGo/History/Model/Visit.swift
deleted file mode 100644
index 6b646a1197..0000000000
--- a/DuckDuckGo/History/Model/Visit.swift
+++ /dev/null
@@ -1,60 +0,0 @@
-//
-// Visit.swift
-//
-// Copyright Β© 2022 DuckDuckGo. All rights reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-
-import Foundation
-
-final class Visit: Stored {
-
- typealias ID = URL
-
- init(date: Date, identifier: ID? = nil, historyEntry: HistoryEntry? = nil) {
- self.date = date
- self.identifier = identifier
- self.historyEntry = historyEntry
- }
-
- let date: Date
-
- var identifier: ID?
- weak var historyEntry: HistoryEntry?
-
-}
-
-extension Visit: Hashable {
-
- static func == (lhs: Visit, rhs: Visit) -> Bool {
- lhs === rhs
- }
-
- func hash(into hasher: inout Hasher) {
- hasher.combine(ObjectIdentifier(self))
- }
-
-}
-
-extension Visit: NSCopying {
-
- func copy(with zone: NSZone? = nil) -> Any {
- let visit = Visit(date: date,
- identifier: identifier,
- historyEntry: nil)
- visit.savingState = savingState
- return visit
- }
-
-}
diff --git a/DuckDuckGo/History/Services/HistoryStore.swift b/DuckDuckGo/History/Services/EncryptedHistoryStore.swift
similarity index 96%
rename from DuckDuckGo/History/Services/HistoryStore.swift
rename to DuckDuckGo/History/Services/EncryptedHistoryStore.swift
index ca5695fb9c..68cbbea58a 100644
--- a/DuckDuckGo/History/Services/HistoryStore.swift
+++ b/DuckDuckGo/History/Services/EncryptedHistoryStore.swift
@@ -1,5 +1,5 @@
//
-// HistoryStore.swift
+// EncryptedHistoryStore.swift
//
// Copyright Β© 2021 DuckDuckGo. All rights reserved.
//
@@ -20,19 +20,9 @@ import Common
import Foundation
import CoreData
import Combine
+import History
-protocol HistoryStoring {
-
- func cleanOld(until date: Date) -> Future
- func save(entry: HistoryEntry) -> Future<[(id: Visit.ID, date: Date)], Error>
- func removeEntries(_ entries: [HistoryEntry]) -> Future
- func removeVisits(_ visits: [Visit]) -> Future
-
-}
-
-final class HistoryStore: HistoryStoring {
-
- init() {}
+final class EncryptedHistoryStore: HistoryStoring {
init(context: NSManagedObjectContext) {
self.context = context
@@ -43,7 +33,7 @@ final class HistoryStore: HistoryStoring {
case savingFailed
}
- private lazy var context = Database.shared.makeContext(concurrencyType: .privateQueueConcurrencyType, name: "History")
+ let context: NSManagedObjectContext
func removeEntries(_ entries: [HistoryEntry]) -> Future {
return Future { [weak self] promise in
diff --git a/DuckDuckGo/History/Services/History.xcdatamodeld/History 6.xcdatamodel/contents b/DuckDuckGo/History/Services/History.xcdatamodeld/History 6.xcdatamodel/contents
index 2c95affdfd..e43f42a783 100644
--- a/DuckDuckGo/History/Services/History.xcdatamodeld/History 6.xcdatamodel/contents
+++ b/DuckDuckGo/History/Services/History.xcdatamodeld/History 6.xcdatamodel/contents
@@ -1,5 +1,5 @@
-
+
@@ -7,17 +7,21 @@
-
+
+
+
+
+
-
+
+
+
+
+
-
-
-
-
\ No newline at end of file
diff --git a/DuckDuckGo/History/Services/HistoryCoordinatorExtension.swift b/DuckDuckGo/History/Services/HistoryCoordinatorExtension.swift
new file mode 100644
index 0000000000..9ca366fb72
--- /dev/null
+++ b/DuckDuckGo/History/Services/HistoryCoordinatorExtension.swift
@@ -0,0 +1,56 @@
+//
+// HistoryCoordinatorExtension.swift
+//
+// Copyright Β© 2024 DuckDuckGo. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import Foundation
+import History
+
+extension HistoryCoordinator {
+
+ static let shared = HistoryCoordinator(historyStoring: EncryptedHistoryStore(context: Database.shared.makeContext(concurrencyType: .privateQueueConcurrencyType, name: "History")))
+
+ func migrateModelV5toV6IfNeeded() {
+ let defaults = MigrationDefaults()
+
+ guard let historyDictionary = historyDictionary,
+ !defaults.historyV5toV6Migration else {
+ return
+ }
+
+ defaults.historyV5toV6Migration = true
+
+ for entry in historyDictionary.values where entry.visits.isEmpty {
+ entry.addOldVisit(date: entry.lastVisit)
+ save(entry: entry)
+ }
+ }
+
+ final class MigrationDefaults {
+ @UserDefaultsWrapper(key: .historyV5toV6Migration, defaultValue: false)
+ var historyV5toV6Migration: Bool
+ }
+
+}
+
+extension HistoryEntry {
+
+ func addOldVisit(date: Date) {
+ let visit = Visit(date: date, historyEntry: self)
+ visits.insert(visit)
+ }
+
+}
diff --git a/DuckDuckGo/History/ViewModel/VisitViewModel.swift b/DuckDuckGo/History/ViewModel/VisitViewModel.swift
index c8cd8e6ed7..5dcef2fc44 100644
--- a/DuckDuckGo/History/ViewModel/VisitViewModel.swift
+++ b/DuckDuckGo/History/ViewModel/VisitViewModel.swift
@@ -17,6 +17,7 @@
//
import Cocoa
+import History
final class VisitViewModel {
diff --git a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift
index 93e8b9ee74..49c3dd9fad 100644
--- a/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift
+++ b/DuckDuckGo/HomePage/Model/HomePageContinueSetUpModel.swift
@@ -281,6 +281,7 @@ extension HomePage.Models {
}
// Helper Functions
+ @MainActor(unsafe)
@objc private func newTabOpenNotification(_ notification: Notification) {
if !isFirstSession {
listOfFeatures = randomisedFeatures
diff --git a/DuckDuckGo/HomePage/Model/HomePageRecentlyVisitedModel.swift b/DuckDuckGo/HomePage/Model/HomePageRecentlyVisitedModel.swift
index f92a544eaf..3c57e55f26 100644
--- a/DuckDuckGo/HomePage/Model/HomePageRecentlyVisitedModel.swift
+++ b/DuckDuckGo/HomePage/Model/HomePageRecentlyVisitedModel.swift
@@ -18,6 +18,7 @@
import Foundation
import SwiftUI
+import History
extension HomePage.Models {
diff --git a/DuckDuckGo/HomePage/View/HomePageViewController.swift b/DuckDuckGo/HomePage/View/HomePageViewController.swift
index 6317449878..ae40aff5df 100644
--- a/DuckDuckGo/HomePage/View/HomePageViewController.swift
+++ b/DuckDuckGo/HomePage/View/HomePageViewController.swift
@@ -19,6 +19,7 @@
import Cocoa
import Combine
import SwiftUI
+import History
@MainActor
final class HomePageViewController: NSViewController {
diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings
index bb676d1c1b..602af4dda6 100644
--- a/DuckDuckGo/Localizable.xcstrings
+++ b/DuckDuckGo/Localizable.xcstrings
@@ -159,6 +159,17 @@
}
}
},
+ "%@ of %@" : {
+ "comment" : "Number of bytes out of total bytes downloaded (1Mb of 2Mb)",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "%1$@ of %2$@"
+ }
+ }
+ }
+ },
"%lld" : {
"localizations" : {
"de" : {
@@ -12931,6 +12942,18 @@
}
}
},
+ "downloads.bytes.format" : {
+ "comment" : "Number of bytes out of total bytes downloaded (1Mb of 2Mb)",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "%1$@ of %2$@"
+ }
+ }
+ }
+ },
"downloads.change" : {
"comment" : "Change downloads directory button",
"extractionState" : "extracted_with_value",
@@ -13711,6 +13734,18 @@
}
}
},
+ "downloads.open.on.completion" : {
+ "comment" : "Checkbox to open a Download Manager popover when downloads are completed",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "Automatically open the Downloads panel when downloads complete"
+ }
+ }
+ }
+ },
"downloads.remove-from-list.item" : {
"comment" : "Contextual menu item in downloads manager to remove the given downloaded from the list of downloaded files",
"extractionState" : "extracted_with_value",
@@ -13891,6 +13926,18 @@
}
}
},
+ "downloads.speed.format" : {
+ "comment" : "Download speed format (1Mb/sec)",
+ "extractionState" : "extracted_with_value",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "new",
+ "value" : "%@/s"
+ }
+ }
+ }
+ },
"downloads.stop.item" : {
"comment" : "Contextual menu item in downloads manager to stop the download",
"extractionState" : "extracted_with_value",
@@ -49537,4 +49584,4 @@
}
},
"version" : "1.0"
-}
\ No newline at end of file
+}
diff --git a/DuckDuckGo/MainWindow/MainView.swift b/DuckDuckGo/MainWindow/MainView.swift
index 6ef7452e5d..5dc064ac99 100644
--- a/DuckDuckGo/MainWindow/MainView.swift
+++ b/DuckDuckGo/MainWindow/MainView.swift
@@ -18,6 +18,7 @@
import Cocoa
import Combine
+import WebKit
final class MainView: NSView {
let tabBarContainerView = NSView()
@@ -107,6 +108,8 @@ final class MainView: NSView {
// PDF Plugin context menu
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
setupSearchContextMenuItem(menu: menu)
+ setupSaveAsAndPrintMenuItems(menu: menu, with: event)
+ super.willOpenMenu(menu, with: event)
}
private func setupSearchContextMenuItem(menu: NSMenu) {
@@ -130,6 +133,45 @@ final class MainView: NSView {
}
}
+ private func setupSaveAsAndPrintMenuItems(menu: NSMenu, with event: NSEvent) {
+ guard let window else { return }
+
+ // try to find PDF HUD view at the right-click location (it might be a frame click)
+ let hudView: WKPDFHUDViewWrapper? = {
+ for point in [event.locationInWindow, window.mouseLocationOutsideOfEventStream] {
+ let locationInView = convert(point, from: nil)
+ guard let view = self.hitTest(locationInView) else { continue }
+
+ if let hudView = WKPDFHUDViewWrapper(view: view) {
+ return hudView
+ } else if let webView = view as? WKWebView,
+ let hudView = webView.hudView(at: webView.convert(locationInView, from: self)) {
+ return hudView
+ }
+ }
+ return (self.hitTest(bounds.center) as? WKWebView)?.hudView()
+ }()
+ assert(hudView != nil)
+
+ // insert Save As⦠and Print⦠items after `Open with Preview`
+ // 1. find `Copy`
+ let idxAfterCopy = menu.indexOfItem(withTitle: UserText.copy) + /* will become 0 if no copy (-1 + 1) */ 1
+ let insertionIdx: Int
+ if idxAfterCopy > 0 {
+ // 2. find separator below `Copy`
+ let separatorIdx = (idxAfterCopy.. NSDragOperation {
diff --git a/DuckDuckGo/MainWindow/MainViewController.swift b/DuckDuckGo/MainWindow/MainViewController.swift
index 3b25f0524c..53fb90c68b 100644
--- a/DuckDuckGo/MainWindow/MainViewController.swift
+++ b/DuckDuckGo/MainWindow/MainViewController.swift
@@ -481,6 +481,13 @@ extension MainViewController {
.subtracting(.capsLock)
switch Int(event.keyCode) {
+ case kVK_Return where navigationBarViewController.addressBarViewController?
+ .addressBarTextField.isFirstResponder == true:
+
+ navigationBarViewController.addressBarViewController?.addressBarTextField.addressBarEnterPressed()
+
+ return true
+
case kVK_Escape:
var isHandled = false
if !mainView.findInPageContainerView.isHidden {
diff --git a/DuckDuckGo/MainWindow/MainWindowController.swift b/DuckDuckGo/MainWindow/MainWindowController.swift
index fd42a89970..a5830b5053 100644
--- a/DuckDuckGo/MainWindow/MainWindowController.swift
+++ b/DuckDuckGo/MainWindow/MainWindowController.swift
@@ -176,7 +176,11 @@ final class MainWindowController: NSWindowController {
}
func orderWindowBack(_ sender: Any?) {
- window?.orderBack(sender)
+ if let lastKeyWindow = WindowControllersManager.shared.lastKeyMainWindowController?.window {
+ window?.order(.below, relativeTo: lastKeyWindow.windowNumber)
+ } else {
+ window?.orderFront(sender)
+ }
register()
}
diff --git a/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift b/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift
index f21f662eea..202bee5105 100644
--- a/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift
+++ b/DuckDuckGo/Menus/CleanThisHistoryMenuItem.swift
@@ -18,6 +18,7 @@
import AppKit
import Foundation
+import History
final class ClearThisHistoryMenuItem: NSMenuItem {
diff --git a/DuckDuckGo/Menus/HistoryMenu.swift b/DuckDuckGo/Menus/HistoryMenu.swift
index 9351d1e578..aca38c3e10 100644
--- a/DuckDuckGo/Menus/HistoryMenu.swift
+++ b/DuckDuckGo/Menus/HistoryMenu.swift
@@ -19,6 +19,7 @@
import Cocoa
import Combine
import Common
+import History
@MainActor
final class HistoryMenu: NSMenu {
diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift
index 7d97cd4284..e27e55b644 100644
--- a/DuckDuckGo/Menus/MainMenu.swift
+++ b/DuckDuckGo/Menus/MainMenu.swift
@@ -41,11 +41,11 @@ import SubscriptionUI
static let maxTitleLength = 55
}
- // MARK: - DuckDuckGo
+ // MARK: DuckDuckGo
let servicesMenu = NSMenu(title: UserText.mainMenuAppServices)
let preferencesMenuItem = NSMenuItem(title: UserText.mainMenuAppPreferences, action: #selector(AppDelegate.openPreferences), keyEquivalent: ",")
- // MARK: - File
+ // MARK: File
let newWindowMenuItem = NSMenuItem(title: UserText.newWindowMenuItem, action: #selector(AppDelegate.newWindow), keyEquivalent: "n")
let newTabMenuItem = NSMenuItem(title: UserText.mainMenuFileNewTab, action: #selector(AppDelegate.newTab), keyEquivalent: "t")
let openLocationMenuItem = NSMenuItem(title: UserText.mainMenuFileOpenLocation, action: #selector(AppDelegate.openLocation), keyEquivalent: "l")
@@ -56,7 +56,7 @@ import SubscriptionUI
let sharingMenu = SharingMenu(title: UserText.shareMenuItem)
- // MARK: - View
+ // MARK: View
let stopMenuItem = NSMenuItem(title: UserText.mainMenuViewStop, action: #selector(MainViewController.stopLoadingPage), keyEquivalent: ".")
let reloadMenuItem = NSMenuItem(title: UserText.mainMenuViewReloadPage, action: #selector(MainViewController.reloadPage), keyEquivalent: "r")
@@ -65,13 +65,13 @@ import SubscriptionUI
let zoomInMenuItem = NSMenuItem(title: UserText.mainMenuViewZoomIn, action: #selector(MainViewController.zoomIn), keyEquivalent: "+")
let zoomOutMenuItem = NSMenuItem(title: UserText.mainMenuViewZoomOut, action: #selector(MainViewController.zoomOut), keyEquivalent: "-")
- // MARK: - History
+ // MARK: History
let historyMenu = HistoryMenu()
var backMenuItem: NSMenuItem { historyMenu.backMenuItem }
var forwardMenuItem: NSMenuItem { historyMenu.forwardMenuItem }
- // MARK: - Bookmarks
+ // MARK: Bookmarks
let manageBookmarksMenuItem = NSMenuItem(title: UserText.mainMenuHistoryManageBookmarks, action: #selector(MainViewController.showManageBookmarks))
var bookmarksMenuToggleBookmarksBarMenuItem = NSMenuItem(title: "BookmarksBarMenuPlaceholder", action: #selector(MainViewController.toggleBookmarksBarFromMenu), keyEquivalent: "B")
let importBookmarksMenuItem = NSMenuItem(title: UserText.importBookmarks, action: #selector(AppDelegate.openImportBrowserDataWindow))
@@ -89,17 +89,17 @@ import SubscriptionUI
let toggleNetworkProtectionShortcutMenuItem = NSMenuItem(title: UserText.showNetworkProtectionShortcut, action: #selector(MainViewController.toggleNetworkProtectionShortcut), keyEquivalent: "N")
#endif
- // MARK: - Window
+ // MARK: Window
let windowsMenu = NSMenu(title: UserText.mainMenuWindow)
- // MARK: - Debug
+ // MARK: Debug
private var loggingMenu: NSMenu?
let customConfigurationUrlMenuItem = NSMenuItem(title: "Last Update Time", action: nil)
let configurationDateAndTimeMenuItem = NSMenuItem(title: "Configuration URL", action: nil)
let autofillDebugScriptMenuItem = NSMenuItem(title: "Autofill Debug Script", action: #selector(MainMenu.toggleAutofillScriptDebugSettingsAction))
- // MARK: - Help
+ // MARK: Help
let helpMenu = NSMenu(title: UserText.mainMenuHelp) {
NSMenuItem(title: UserText.mainMenuHelpDuckDuckGoHelp, action: #selector(NSApplication.showHelp), keyEquivalent: "?")
@@ -111,259 +111,281 @@ import SubscriptionUI
#endif
}
- // swiftlint:disable:next function_body_length
+ // MARK: - Initialization
+
init(featureFlagger: FeatureFlagger, bookmarkManager: BookmarkManager, faviconManager: FaviconManagement, copyHandler: CopyHandler) {
super.init(title: UserText.duckDuckGo)
buildItems {
- // MARK: DuckDuckGo
- NSMenuItem(title: UserText.duckDuckGo) {
- NSMenuItem(title: UserText.aboutDuckDuckGo, action: #selector(AppDelegate.openAbout))
- NSMenuItem.separator()
+ buildDuckDuckGoMenu()
+ buildFileMenu()
+ buildEditMenu(copyHandler: copyHandler)
+ buildViewMenu()
+ buildHistoryMenu()
+ buildBookmarksMenu()
+ buildWindowMenu()
+ buildDebugMenu(featureFlagger: featureFlagger)
+ buildHelpMenu()
+ }
- preferencesMenuItem
+ subscribeToBookmarkList(bookmarkManager: bookmarkManager)
+ subscribeToFavicons(faviconManager: faviconManager)
+ }
- NSMenuItem.separator()
+ func buildDuckDuckGoMenu() -> NSMenuItem {
+ NSMenuItem(title: UserText.duckDuckGo) {
+ NSMenuItem(title: UserText.aboutDuckDuckGo, action: #selector(AppDelegate.openAbout))
+ NSMenuItem.separator()
- NSMenuItem(title: UserText.mainMenuAppServices)
- .submenu(servicesMenu)
- NSMenuItem.separator()
+ preferencesMenuItem
+
+ NSMenuItem.separator()
+
+ NSMenuItem(title: UserText.mainMenuAppServices)
+ .submenu(servicesMenu)
+ NSMenuItem.separator()
#if SPARKLE
- NSMenuItem(title: UserText.mainMenuAppCheckforUpdates, action: #selector(AppDelegate.checkForUpdates))
- NSMenuItem.separator()
+ NSMenuItem(title: UserText.mainMenuAppCheckforUpdates, action: #selector(AppDelegate.checkForUpdates))
+ NSMenuItem.separator()
#endif
- NSMenuItem(title: UserText.mainMenuAppHideDuckDuckGo, action: #selector(NSApplication.hide), keyEquivalent: "h")
- NSMenuItem(title: UserText.mainMenuAppHideOthers, action: #selector(NSApplication.hideOtherApplications), keyEquivalent: [.option, .command, "h"])
- NSMenuItem(title: UserText.mainMenuAppShowAll, action: #selector(NSApplication.unhideAllApplications))
- NSMenuItem.separator()
+ NSMenuItem(title: UserText.mainMenuAppHideDuckDuckGo, action: #selector(NSApplication.hide), keyEquivalent: "h")
+ NSMenuItem(title: UserText.mainMenuAppHideOthers, action: #selector(NSApplication.hideOtherApplications), keyEquivalent: [.option, .command, "h"])
+ NSMenuItem(title: UserText.mainMenuAppShowAll, action: #selector(NSApplication.unhideAllApplications))
+ NSMenuItem.separator()
- NSMenuItem(title: UserText.mainMenuAppQuitDuckDuckGo, action: #selector(NSApplication.terminate), keyEquivalent: "q")
- }
+ NSMenuItem(title: UserText.mainMenuAppQuitDuckDuckGo, action: #selector(NSApplication.terminate), keyEquivalent: "q")
+ }
+ }
- // MARK: File
- NSMenuItem(title: UserText.mainMenuFile) {
- newWindowMenuItem
- NSMenuItem(title: UserText.newBurnerWindowMenuItem, action: #selector(AppDelegate.newBurnerWindow), keyEquivalent: "N")
- newTabMenuItem
- openLocationMenuItem
- NSMenuItem.separator()
+ func buildFileMenu() -> NSMenuItem {
+ NSMenuItem(title: UserText.mainMenuFile) {
+ newWindowMenuItem
+ NSMenuItem(title: UserText.newBurnerWindowMenuItem, action: #selector(AppDelegate.newBurnerWindow), keyEquivalent: "N")
+ newTabMenuItem
+ openLocationMenuItem
+ NSMenuItem.separator()
+
+ closeWindowMenuItem
+ closeAllWindowsMenuItem
+ closeTabMenuItem
+ NSMenuItem(title: UserText.mainMenuFileSaveAs, action: #selector(MainViewController.saveAs), keyEquivalent: "s")
+ NSMenuItem.separator()
+
+ importBrowserDataMenuItem
+ NSMenuItem(title: UserText.mainMenuFileExport) {
+ NSMenuItem(title: UserText.mainMenuFileExportPasswords, action: #selector(AppDelegate.openExportLogins))
+ NSMenuItem(title: UserText.mainMenuFileExportBookmarks, action: #selector(AppDelegate.openExportBookmarks))
+ }
+ NSMenuItem.separator()
- closeWindowMenuItem
- closeAllWindowsMenuItem
- closeTabMenuItem
- NSMenuItem(title: UserText.mainMenuFileSaveAs, action: #selector(MainViewController.saveAs), keyEquivalent: "s")
- NSMenuItem.separator()
+ NSMenuItem(title: UserText.shareMenuItem)
+ .submenu(sharingMenu)
+ NSMenuItem.separator()
- importBrowserDataMenuItem
- NSMenuItem(title: UserText.mainMenuFileExport) {
- NSMenuItem(title: UserText.mainMenuFileExportPasswords, action: #selector(AppDelegate.openExportLogins))
- NSMenuItem(title: UserText.mainMenuFileExportBookmarks, action: #selector(AppDelegate.openExportBookmarks))
- }
- NSMenuItem.separator()
+ NSMenuItem(title: UserText.printMenuItem, action: #selector(MainViewController.printWebView), keyEquivalent: "p")
+ }
+ }
- NSMenuItem(title: UserText.shareMenuItem)
- .submenu(sharingMenu)
+ func buildEditMenu(copyHandler: CopyHandler) -> NSMenuItem {
+ NSMenuItem(title: UserText.mainMenuEdit) {
+ NSMenuItem(title: UserText.mainMenuEditUndo, action: Selector(("undo:")), keyEquivalent: "z")
+ NSMenuItem(title: UserText.mainMenuEditRedo, action: Selector(("redo:")), keyEquivalent: "Z")
+ NSMenuItem.separator()
+
+ NSMenuItem(title: UserText.mainMenuEditCut, action: #selector(NSText.cut), keyEquivalent: "x")
+ NSMenuItem(title: UserText.mainMenuEditCopy, action: #selector(CopyHandler.copy(_:)), target: copyHandler, keyEquivalent: "c")
+ NSMenuItem(title: UserText.mainMenuEditPaste, action: #selector(NSText.paste), keyEquivalent: "v")
+ NSMenuItem(title: UserText.mainMenuEditPasteAndMatchStyle, action: #selector(NSTextView.pasteAsPlainText), keyEquivalent: [.option, .command, .shift, "v"])
+ NSMenuItem(title: UserText.mainMenuEditPasteAndMatchStyle, action: #selector(NSTextView.pasteAsPlainText), keyEquivalent: [.command, .shift, "v"])
+ .alternate()
+
+ NSMenuItem(title: UserText.mainMenuEditDelete, action: #selector(NSText.delete))
+ NSMenuItem(title: UserText.mainMenuEditSelectAll, action: #selector(NSText.selectAll), keyEquivalent: "a")
+ NSMenuItem.separator()
+
+ NSMenuItem(title: UserText.mainMenuEditFind) {
+ NSMenuItem(title: UserText.findInPageMenuItem, action: #selector(MainViewController.findInPage), keyEquivalent: "f")
+ NSMenuItem(title: UserText.mainMenuEditFindFindNext, action: #selector(MainViewController.findInPageNext), keyEquivalent: "g")
+ NSMenuItem(title: UserText.mainMenuEditFindFindPrevious, action: #selector(MainViewController.findInPagePrevious), keyEquivalent: "G")
NSMenuItem.separator()
- NSMenuItem(title: UserText.printMenuItem, action: #selector(MainViewController.printWebView), keyEquivalent: "p")
+ NSMenuItem(title: UserText.mainMenuEditFindHideFind, action: #selector(MainViewController.findInPageDone), keyEquivalent: "F")
}
- // MARK: Edit
- NSMenuItem(title: UserText.mainMenuEdit) {
- NSMenuItem(title: UserText.mainMenuEditUndo, action: Selector(("undo:")), keyEquivalent: "z")
- NSMenuItem(title: UserText.mainMenuEditRedo, action: Selector(("redo:")), keyEquivalent: "Z")
+ NSMenuItem(title: UserText.mainMenuEditSpellingandGrammar) {
+ NSMenuItem(title: UserText.mainMenuEditSpellingandShowSpellingandGrammar, action: #selector(NSText.showGuessPanel), keyEquivalent: ":")
+ NSMenuItem(title: UserText.mainMenuEditSpellingandCheckDocumentNow, action: #selector(NSText.checkSpelling), keyEquivalent: ";")
NSMenuItem.separator()
- NSMenuItem(title: UserText.mainMenuEditCut, action: #selector(NSText.cut), keyEquivalent: "x")
- NSMenuItem(title: UserText.mainMenuEditCopy, action: #selector(CopyHandler.copy(_:)), target: copyHandler, keyEquivalent: "c")
- NSMenuItem(title: UserText.mainMenuEditPaste, action: #selector(NSText.paste), keyEquivalent: "v")
- NSMenuItem(title: UserText.mainMenuEditPasteAndMatchStyle, action: #selector(NSTextView.pasteAsPlainText), keyEquivalent: [.option, .command, .shift, "v"])
- NSMenuItem(title: UserText.mainMenuEditPasteAndMatchStyle, action: #selector(NSTextView.pasteAsPlainText), keyEquivalent: [.command, .shift, "v"])
- .alternate()
+ NSMenuItem(title: UserText.mainMenuEditSpellingandCheckSpellingWhileTyping, action: #selector(NSTextView.toggleContinuousSpellChecking))
+ NSMenuItem(title: UserText.mainMenuEditSpellingandCheckGrammarWithSpelling, action: #selector(NSTextView.toggleGrammarChecking))
+ NSMenuItem(title: UserText.mainMenuEditSpellingandCorrectSpellingAutomatically, action: #selector(NSTextView.toggleAutomaticSpellingCorrection))
+ .hidden()
+ }
- NSMenuItem(title: UserText.mainMenuEditDelete, action: #selector(NSText.delete))
- NSMenuItem(title: UserText.mainMenuEditSelectAll, action: #selector(NSText.selectAll), keyEquivalent: "a")
+ NSMenuItem(title: UserText.mainMenuEditSubstitutions) {
+ NSMenuItem(title: UserText.mainMenuEditSubstitutionsShowSubstitutions, action: #selector(NSTextView.orderFrontSubstitutionsPanel))
NSMenuItem.separator()
- NSMenuItem(title: UserText.mainMenuEditFind) {
- NSMenuItem(title: UserText.findInPageMenuItem, action: #selector(MainViewController.findInPage), keyEquivalent: "f")
- NSMenuItem(title: UserText.mainMenuEditFindFindNext, action: #selector(MainViewController.findInPageNext), keyEquivalent: "g")
- NSMenuItem(title: UserText.mainMenuEditFindFindPrevious, action: #selector(MainViewController.findInPagePrevious), keyEquivalent: "G")
- NSMenuItem.separator()
-
- NSMenuItem(title: UserText.mainMenuEditFindHideFind, action: #selector(MainViewController.findInPageDone), keyEquivalent: "F")
- }
-
- NSMenuItem(title: UserText.mainMenuEditSpellingandGrammar) {
- NSMenuItem(title: UserText.mainMenuEditSpellingandShowSpellingandGrammar, action: #selector(NSText.showGuessPanel), keyEquivalent: ":")
- NSMenuItem(title: UserText.mainMenuEditSpellingandCheckDocumentNow, action: #selector(NSText.checkSpelling), keyEquivalent: ";")
- NSMenuItem.separator()
-
- NSMenuItem(title: UserText.mainMenuEditSpellingandCheckSpellingWhileTyping, action: #selector(NSTextView.toggleContinuousSpellChecking))
- NSMenuItem(title: UserText.mainMenuEditSpellingandCheckGrammarWithSpelling, action: #selector(NSTextView.toggleGrammarChecking))
- NSMenuItem(title: UserText.mainMenuEditSpellingandCorrectSpellingAutomatically, action: #selector(NSTextView.toggleAutomaticSpellingCorrection))
- .hidden()
- }
-
- NSMenuItem(title: UserText.mainMenuEditSubstitutions) {
- NSMenuItem(title: UserText.mainMenuEditSubstitutionsShowSubstitutions, action: #selector(NSTextView.orderFrontSubstitutionsPanel))
- NSMenuItem.separator()
-
- NSMenuItem(title: UserText.mainMenuEditSubstitutionsSmartCopyPaste, action: #selector(NSTextView.toggleSmartInsertDelete))
- NSMenuItem(title: UserText.mainMenuEditSubstitutionsSmartQuotes, action: #selector(NSTextView.toggleAutomaticQuoteSubstitution))
- NSMenuItem(title: UserText.mainMenuEditSubstitutionsSmartDashes, action: #selector(NSTextView.toggleAutomaticDashSubstitution))
- NSMenuItem(title: UserText.mainMenuEditSubstitutionsSmartLinks, action: #selector(NSTextView.toggleAutomaticLinkDetection))
- NSMenuItem(title: UserText.mainMenuEditSubstitutionsDataDetectors, action: #selector(NSTextView.toggleAutomaticDataDetection))
- NSMenuItem(title: UserText.mainMenuEditSubstitutionsTextReplacement, action: #selector(NSTextView.toggleAutomaticTextReplacement))
- }
+ NSMenuItem(title: UserText.mainMenuEditSubstitutionsSmartCopyPaste, action: #selector(NSTextView.toggleSmartInsertDelete))
+ NSMenuItem(title: UserText.mainMenuEditSubstitutionsSmartQuotes, action: #selector(NSTextView.toggleAutomaticQuoteSubstitution))
+ NSMenuItem(title: UserText.mainMenuEditSubstitutionsSmartDashes, action: #selector(NSTextView.toggleAutomaticDashSubstitution))
+ NSMenuItem(title: UserText.mainMenuEditSubstitutionsSmartLinks, action: #selector(NSTextView.toggleAutomaticLinkDetection))
+ NSMenuItem(title: UserText.mainMenuEditSubstitutionsDataDetectors, action: #selector(NSTextView.toggleAutomaticDataDetection))
+ NSMenuItem(title: UserText.mainMenuEditSubstitutionsTextReplacement, action: #selector(NSTextView.toggleAutomaticTextReplacement))
+ }
- NSMenuItem(title: UserText.mainMenuEditTransformations) {
- NSMenuItem(title: UserText.mainMenuEditTransformationsMakeUpperCase, action: #selector(NSResponder.uppercaseWord))
- NSMenuItem(title: UserText.mainMenuEditTransformationsMakeLowerCase, action: #selector(NSResponder.lowercaseWord))
- NSMenuItem(title: UserText.mainMenuEditTransformationsCapitalize, action: #selector(NSResponder.capitalizeWord))
- }
+ NSMenuItem(title: UserText.mainMenuEditTransformations) {
+ NSMenuItem(title: UserText.mainMenuEditTransformationsMakeUpperCase, action: #selector(NSResponder.uppercaseWord))
+ NSMenuItem(title: UserText.mainMenuEditTransformationsMakeLowerCase, action: #selector(NSResponder.lowercaseWord))
+ NSMenuItem(title: UserText.mainMenuEditTransformationsCapitalize, action: #selector(NSResponder.capitalizeWord))
+ }
- NSMenuItem(title: UserText.mainMenuEditSpeech) {
- NSMenuItem(title: UserText.mainMenuEditSpeechStartSpeaking, action: #selector(NSTextView.startSpeaking))
- NSMenuItem(title: UserText.mainMenuEditSpeechStopSpeaking, action: #selector(NSTextView.stopSpeaking))
- }
+ NSMenuItem(title: UserText.mainMenuEditSpeech) {
+ NSMenuItem(title: UserText.mainMenuEditSpeechStartSpeaking, action: #selector(NSTextView.startSpeaking))
+ NSMenuItem(title: UserText.mainMenuEditSpeechStopSpeaking, action: #selector(NSTextView.stopSpeaking))
}
+ }
+ }
- // MARK: View
- NSMenuItem(title: UserText.mainMenuView) {
- stopMenuItem
- reloadMenuItem
- NSMenuItem.separator()
+ func buildViewMenu() -> NSMenuItem {
+ NSMenuItem(title: UserText.mainMenuView) {
+ stopMenuItem
+ reloadMenuItem
+ NSMenuItem.separator()
- NSMenuItem(title: UserText.mainMenuViewHome, action: #selector(MainViewController.home), keyEquivalent: "H")
- NSMenuItem.separator()
+ NSMenuItem(title: UserText.mainMenuViewHome, action: #selector(MainViewController.home), keyEquivalent: "H")
+ NSMenuItem.separator()
- toggleBookmarksBarMenuItem
+ toggleBookmarksBarMenuItem
- NSMenuItem(title: UserText.openDownloads, action: #selector(MainViewController.toggleDownloads), keyEquivalent: "j")
- NSMenuItem.separator()
+ NSMenuItem(title: UserText.openDownloads, action: #selector(MainViewController.toggleDownloads), keyEquivalent: "j")
+ NSMenuItem.separator()
- homeButtonMenuItem
- toggleAutofillShortcutMenuItem
- toggleBookmarksShortcutMenuItem
- toggleDownloadsShortcutMenuItem
+ homeButtonMenuItem
+ toggleAutofillShortcutMenuItem
+ toggleBookmarksShortcutMenuItem
+ toggleDownloadsShortcutMenuItem
#if NETWORK_PROTECTION
- toggleNetworkProtectionShortcutMenuItem
+ toggleNetworkProtectionShortcutMenuItem
#endif
- NSMenuItem.separator()
+ NSMenuItem.separator()
- toggleFullscreenMenuItem
- NSMenuItem.separator()
+ toggleFullscreenMenuItem
+ NSMenuItem.separator()
- actualSizeMenuItem
- zoomInMenuItem
- zoomOutMenuItem
- NSMenuItem.separator()
+ actualSizeMenuItem
+ zoomInMenuItem
+ zoomOutMenuItem
+ NSMenuItem.separator()
- NSMenuItem(title: UserText.mainMenuDeveloper) {
- NSMenuItem(title: UserText.openDeveloperTools, action: #selector(MainViewController.toggleDeveloperTools), keyEquivalent: [.option, .command, "i"])
- NSMenuItem(title: UserText.mainMenuViewDeveloperJavaScriptConsole, action: #selector(MainViewController.openJavaScriptConsole), keyEquivalent: [.option, .command, "c"])
- NSMenuItem(title: UserText.mainMenuViewDeveloperShowPageSource, action: #selector(MainViewController.showPageSource), keyEquivalent: [.option, .command, "u"])
- NSMenuItem(title: UserText.mainMenuViewDeveloperShowResources, action: #selector(MainViewController.showPageResources), keyEquivalent: [.option, .command, "a"])
- }
+ NSMenuItem(title: UserText.mainMenuDeveloper) {
+ NSMenuItem(title: UserText.openDeveloperTools, action: #selector(MainViewController.toggleDeveloperTools), keyEquivalent: [.option, .command, "i"])
+ NSMenuItem(title: UserText.mainMenuViewDeveloperJavaScriptConsole, action: #selector(MainViewController.openJavaScriptConsole), keyEquivalent: [.option, .command, "c"])
+ NSMenuItem(title: UserText.mainMenuViewDeveloperShowPageSource, action: #selector(MainViewController.showPageSource), keyEquivalent: [.option, .command, "u"])
+ NSMenuItem(title: UserText.mainMenuViewDeveloperShowResources, action: #selector(MainViewController.showPageResources), keyEquivalent: [.option, .command, "a"])
}
+ }
+ }
- // MARK: History
- NSMenuItem(title: UserText.mainMenuHistory)
- .submenu(historyMenu)
-
- // MARK: Bookmarks
- NSMenuItem(title: UserText.bookmarks).submenu(bookmarksMenu.buildItems {
- NSMenuItem(title: UserText.bookmarkThisPage, action: #selector(MainViewController.bookmarkThisPage), keyEquivalent: "d")
- manageBookmarksMenuItem
- bookmarksMenuToggleBookmarksBarMenuItem
- NSMenuItem.separator()
+ func buildHistoryMenu() -> NSMenuItem {
+ NSMenuItem(title: UserText.mainMenuHistory)
+ .submenu(historyMenu)
+ }
- importBookmarksMenuItem
- NSMenuItem(title: UserText.exportBookmarks, action: #selector(AppDelegate.openExportBookmarks))
- NSMenuItem.separator()
+ func buildBookmarksMenu() -> NSMenuItem {
+ NSMenuItem(title: UserText.bookmarks).submenu(bookmarksMenu.buildItems {
+ NSMenuItem(title: UserText.bookmarkThisPage, action: #selector(MainViewController.bookmarkThisPage), keyEquivalent: "d")
+ manageBookmarksMenuItem
+ bookmarksMenuToggleBookmarksBarMenuItem
+ NSMenuItem.separator()
+
+ importBookmarksMenuItem
+ NSMenuItem(title: UserText.exportBookmarks, action: #selector(AppDelegate.openExportBookmarks))
+ NSMenuItem.separator()
+
+ NSMenuItem(title: UserText.favorites)
+ .submenu(favoritesMenu.buildItems {
+ NSMenuItem(title: UserText.mainMenuHistoryFavoriteThisPage, action: #selector(MainViewController.favoriteThisPage))
+ .withImage(NSImage(named: "Favorite"))
+ NSMenuItem.separator()
+ })
+ .withImage(NSImage(named: "Favorite"))
- NSMenuItem(title: UserText.favorites)
- .submenu(favoritesMenu.buildItems {
- NSMenuItem(title: UserText.mainMenuHistoryFavoriteThisPage, action: #selector(MainViewController.favoriteThisPage))
- .withImage(NSImage(named: "Favorite"))
- NSMenuItem.separator()
- })
- .withImage(NSImage(named: "Favorite"))
+ NSMenuItem.separator()
+ })
+ }
+ func buildWindowMenu() -> NSMenuItem {
+ NSMenuItem(title: UserText.mainMenuWindow)
+ .submenu(windowsMenu.buildItems {
+ NSMenuItem(title: UserText.mainMenuWindowMinimize, action: #selector(NSWindow.performMiniaturize), keyEquivalent: "m")
+ NSMenuItem(title: UserText.zoom, action: #selector(NSWindow.performZoom))
NSMenuItem.separator()
- })
- // MARK: Window
- NSMenuItem(title: UserText.mainMenuWindow)
- .submenu(windowsMenu.buildItems {
- NSMenuItem(title: UserText.mainMenuWindowMinimize, action: #selector(NSWindow.performMiniaturize), keyEquivalent: "m")
- NSMenuItem(title: UserText.zoom, action: #selector(NSWindow.performZoom))
- NSMenuItem.separator()
-
- NSMenuItem(title: UserText.pinTab, action: #selector(MainViewController.pinOrUnpinTab))
- NSMenuItem(title: UserText.moveTabToNewWindow, action: #selector(MainViewController.moveTabToNewWindow))
- NSMenuItem(title: UserText.mainMenuWindowMergeAllWindows, action: #selector(NSWindow.mergeAllWindows))
- NSMenuItem.separator()
+ NSMenuItem(title: UserText.pinTab, action: #selector(MainViewController.pinOrUnpinTab))
+ NSMenuItem(title: UserText.moveTabToNewWindow, action: #selector(MainViewController.moveTabToNewWindow))
+ NSMenuItem(title: UserText.mainMenuWindowMergeAllWindows, action: #selector(NSWindow.mergeAllWindows))
+ NSMenuItem.separator()
- NSMenuItem(title: UserText.mainMenuWindowShowPreviousTab, action: #selector(MainViewController.showPreviousTab), keyEquivalent: [.control, .shift, .tab])
- NSMenuItem(title: "Show Previous Tab (Hidden)", action: #selector(MainViewController.showPreviousTab), keyEquivalent: [.command, .shift, "["])
- .hidden()
- NSMenuItem(title: "Show Previous Tab (Hidden)", action: #selector(MainViewController.showPreviousTab), keyEquivalent: [.option, .command, .left])
- .hidden()
-
- NSMenuItem(title: UserText.mainMenuWindowShowNextTab, action: #selector(MainViewController.showNextTab), keyEquivalent: [.control, .tab])
- NSMenuItem(title: "Show Next Tab (Hidden)", action: #selector(MainViewController.showNextTab), keyEquivalent: [.command, .shift, "]"])
- .hidden()
- NSMenuItem(title: "Show Next Tab (Hidden)", action: #selector(MainViewController.showNextTab), keyEquivalent: [.option, .command, .right])
- .hidden()
-
- NSMenuItem(title: "Show First Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "1")
- .hidden()
- NSMenuItem(title: "Show Second Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "2")
- .hidden()
- NSMenuItem(title: "Show Third Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "3")
- .hidden()
- NSMenuItem(title: "Show Fourth Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "4")
- .hidden()
- NSMenuItem(title: "Show Fifth Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "5")
- .hidden()
- NSMenuItem(title: "Show Sixth Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "6")
- .hidden()
- NSMenuItem(title: "Show Seventh Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "7")
- .hidden()
- NSMenuItem(title: "Show Eighth Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "8")
- .hidden()
- NSMenuItem(title: "Show Ninth Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "9")
- .hidden()
- NSMenuItem.separator()
+ NSMenuItem(title: UserText.mainMenuWindowShowPreviousTab, action: #selector(MainViewController.showPreviousTab), keyEquivalent: [.control, .shift, .tab])
+ NSMenuItem(title: "Show Previous Tab (Hidden)", action: #selector(MainViewController.showPreviousTab), keyEquivalent: [.command, .shift, "["])
+ .hidden()
+ NSMenuItem(title: "Show Previous Tab (Hidden)", action: #selector(MainViewController.showPreviousTab), keyEquivalent: [.option, .command, .left])
+ .hidden()
+
+ NSMenuItem(title: UserText.mainMenuWindowShowNextTab, action: #selector(MainViewController.showNextTab), keyEquivalent: [.control, .tab])
+ NSMenuItem(title: "Show Next Tab (Hidden)", action: #selector(MainViewController.showNextTab), keyEquivalent: [.command, .shift, "]"])
+ .hidden()
+ NSMenuItem(title: "Show Next Tab (Hidden)", action: #selector(MainViewController.showNextTab), keyEquivalent: [.option, .command, .right])
+ .hidden()
+
+ NSMenuItem(title: "Show First Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "1")
+ .hidden()
+ NSMenuItem(title: "Show Second Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "2")
+ .hidden()
+ NSMenuItem(title: "Show Third Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "3")
+ .hidden()
+ NSMenuItem(title: "Show Fourth Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "4")
+ .hidden()
+ NSMenuItem(title: "Show Fifth Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "5")
+ .hidden()
+ NSMenuItem(title: "Show Sixth Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "6")
+ .hidden()
+ NSMenuItem(title: "Show Seventh Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "7")
+ .hidden()
+ NSMenuItem(title: "Show Eighth Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "8")
+ .hidden()
+ NSMenuItem(title: "Show Ninth Tab (Hidden)", action: #selector(MainViewController.showTab), keyEquivalent: "9")
+ .hidden()
+ NSMenuItem.separator()
- NSMenuItem(title: UserText.mainMenuWindowBringAllToFront, action: #selector(NSApplication.arrangeInFront))
- })
+ NSMenuItem(title: UserText.mainMenuWindowBringAllToFront, action: #selector(NSApplication.arrangeInFront))
+ })
+ }
- // MARK: Debug
+ func buildDebugMenu(featureFlagger: FeatureFlagger) -> NSMenuItem? {
#if DEBUG || REVIEW
+ NSMenuItem(title: "Debug")
+ .submenu(setupDebugMenu())
+#else
+ if featureFlagger.isFeatureOn(.debugMenu) {
NSMenuItem(title: "Debug")
.submenu(setupDebugMenu())
-#else
- if featureFlagger.isFeatureOn(.debugMenu) {
- NSMenuItem(title: "Debug")
- .submenu(setupDebugMenu())
- }
-#endif
-
- // MARK: Help
- NSMenuItem(title: UserText.mainMenuHelp)
- .submenu(helpMenu)
+ } else {
+ nil
}
+#endif
+ }
- subscribeToBookmarkList(bookmarkManager: bookmarkManager)
- subscribeToFavicons(faviconManager: faviconManager)
+ func buildHelpMenu() -> NSMenuItem {
+ NSMenuItem(title: UserText.mainMenuHelp)
+ .submenu(helpMenu)
}
required init(coder: NSCoder) {
diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift
index d1e50ed4a9..80bb0ee02a 100644
--- a/DuckDuckGo/Menus/MainMenuActions.swift
+++ b/DuckDuckGo/Menus/MainMenuActions.swift
@@ -21,6 +21,7 @@ import Cocoa
import Common
import WebKit
import Configuration
+import History
// Actions are sent to objects of responder chain
@@ -265,7 +266,16 @@ extension MainViewController {
/// Finds currently active Tab even if itβs playing a Full Screen video
private func getActiveTabAndIndex() -> (tab: Tab, index: TabIndex)? {
- guard let tab = WindowControllersManager.shared.lastKeyMainWindowController?.activeTab else {
+ var tab: Tab? {
+ // popup windows donβt get to lastKeyMainWindowController so try getting their WindowController directly fron a key window
+ if let window = self.view.window,
+ let mainWindowController = window.nextResponder as? MainWindowController,
+ let tab = mainWindowController.activeTab {
+ return tab
+ }
+ return WindowControllersManager.shared.lastKeyMainWindowController?.activeTab
+ }
+ guard let tab else {
assertionFailure("Could not get currently active Tab")
return nil
}
@@ -621,13 +631,15 @@ extension MainViewController {
// MARK: - Printing
@objc func printWebView(_ sender: Any?) {
- getActiveTabAndIndex()?.tab.print()
+ let pdfHUD = (sender as? NSMenuItem)?.pdfHudRepresentedObject // if printing a PDF (may be from a frame context menu)
+ getActiveTabAndIndex()?.tab.print(pdfHUD: pdfHUD)
}
// MARK: - Saving
@objc func saveAs(_ sender: Any) {
- getActiveTabAndIndex()?.tab.saveWebContentAs()
+ let pdfHUD = (sender as? NSMenuItem)?.pdfHudRepresentedObject // if saving a PDF (may be from a frame context menu)
+ getActiveTabAndIndex()?.tab.saveWebContent(pdfHUD: pdfHUD, location: .prompt)
}
// MARK: - Debug
@@ -1017,3 +1029,16 @@ extension AppDelegate: PrivacyDashboardViewControllerSizeDelegate {
privacyDashboardWindow?.setFrame(NSRect(origin: .zero, size: size), display: true, animate: true)
}
}
+
+extension NSMenuItem {
+
+ var pdfHudRepresentedObject: WKPDFHUDViewWrapper? {
+ guard let representedObject = representedObject else { return nil }
+
+ return representedObject as? WKPDFHUDViewWrapper ?? {
+ assertionFailure("Unexpected SaveAs/Print menu item represented object: \(representedObject)")
+ return nil
+ }()
+ }
+
+}
diff --git a/DuckDuckGo/Menus/VisitMenuItem.swift b/DuckDuckGo/Menus/VisitMenuItem.swift
index 9beb7c5fbf..d73c8bbf37 100644
--- a/DuckDuckGo/Menus/VisitMenuItem.swift
+++ b/DuckDuckGo/Menus/VisitMenuItem.swift
@@ -17,6 +17,7 @@
//
import AppKit
+import History
final class VisitMenuItem: NSMenuItem {
diff --git a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift
index 36d445afbf..73ebe99437 100644
--- a/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift
+++ b/DuckDuckGo/NavigationBar/View/AddressBarTextField.swift
@@ -285,22 +285,22 @@ final class AddressBarTextField: NSTextField {
clearUndoManager()
}
- private func addressBarEnterPressed() {
+ func addressBarEnterPressed() {
suggestionContainerViewModel?.clearUserStringValue()
let suggestion = suggestionContainerViewModel?.selectedSuggestionViewModel?.suggestion
- if NSApp.isCommandPressed {
- openNewTab(selected: NSApp.isShiftPressed, suggestion: suggestion)
- } else {
- navigate(suggestion: suggestion)
- }
+ navigate(suggestion: suggestion)
hideSuggestionWindow()
}
private func navigate(suggestion: Suggestion?) {
- hideSuggestionWindow()
- updateTabUrl(suggestion: suggestion)
+ if NSApp.isCommandPressed {
+ openNew(NSApp.isOptionPressed ? .window : .tab, selected: NSApp.isShiftPressed, suggestion: suggestion)
+ } else {
+ hideSuggestionWindow()
+ updateTabUrl(suggestion: suggestion, downloadRequested: NSApp.isOptionPressed && !NSApp.isShiftPressed)
+ }
currentEditor()?.selectAll(self)
}
@@ -322,7 +322,7 @@ final class AddressBarTextField: NSTextField {
}
}
- private func updateTabUrlWithUrl(_ providedUrl: URL, userEnteredValue: String, suggestion: Suggestion?) {
+ private func updateTabUrlWithUrl(_ providedUrl: URL, userEnteredValue: String, downloadRequested: Bool, suggestion: Suggestion?) {
guard let selectedTabViewModel = tabCollectionViewModel.selectedTabViewModel else {
os_log("%s: Selected tab view model is nil", type: .error, className)
return
@@ -355,44 +355,57 @@ final class AddressBarTextField: NSTextField {
#endif
self.window?.makeFirstResponder(nil)
- selectedTabViewModel.tab.setUrl(providedUrl, source: .userEntered(userEnteredValue))
+ selectedTabViewModel.tab.setUrl(providedUrl, source: .userEntered(userEnteredValue, downloadRequested: downloadRequested))
+ if downloadRequested {
+ updateValue(selectedTabViewModel: nil, addressBarString: nil)
+ }
}
- private func updateTabUrl(suggestion: Suggestion?) {
+ private func updateTabUrl(suggestion: Suggestion?, downloadRequested: Bool) {
makeUrl(suggestion: suggestion,
stringValueWithoutSuffix: stringValueWithoutSuffix,
completion: { [weak self] url, userEnteredValue, isUpgraded in
guard let url = url else { return }
- if isUpgraded { self?.updateTabUpgradedToUrl(url) }
- self?.updateTabUrlWithUrl(url, userEnteredValue: userEnteredValue, suggestion: suggestion)
+ if isUpgraded {
+ self?.updateTab(self?.tabCollectionViewModel.selectedTabViewModel?.tab, upgradedTo: url)
+ }
+ self?.updateTabUrlWithUrl(url, userEnteredValue: userEnteredValue, downloadRequested: downloadRequested, suggestion: suggestion)
})
}
- private func updateTabUpgradedToUrl(_ url: URL?) {
- if url == nil { return }
- let tab = tabCollectionViewModel.selectedTabViewModel?.tab
- tab?.setMainFrameConnectionUpgradedTo(url)
+ private func updateTab(_ tab: Tab?, upgradedTo url: URL?) {
+ guard let tab, let url else { return }
+ tab.setMainFrameConnectionUpgradedTo(url)
}
- private func openNewTabWithUrl(_ providedUrl: URL?, userEnteredValue: String, selected: Bool, suggestion: Suggestion?) {
- guard let url = providedUrl else {
- os_log("%s: Making url from address bar string failed", type: .error, className)
- return
- }
-
- let tab = Tab(content: .url(url, source: .userEntered(userEnteredValue)),
- shouldLoadInBackground: true,
- burnerMode: tabCollectionViewModel.burnerMode)
- tabCollectionViewModel.append(tab: tab, selected: selected)
- }
-
- private func openNewTab(selected: Bool, suggestion: Suggestion?) {
+ enum TabOrWindow { case tab, window }
+ private func openNew(_ tabOrWindow: TabOrWindow, selected: Bool, suggestion: Suggestion?) {
makeUrl(suggestion: suggestion,
stringValueWithoutSuffix: stringValueWithoutSuffix) { [weak self] url, userEnteredValue, isUpgraded in
+ guard let self, let url else {
+ os_log("%s: Making url from address bar string failed", type: .error)
+ return
+ }
+ let tab = Tab(content: .url(url, source: .userEntered(userEnteredValue)),
+ shouldLoadInBackground: true,
+ burnerMode: tabCollectionViewModel.burnerMode)
+
+ if isUpgraded {
+ updateTab(tab, upgradedTo: url)
+ }
- if isUpgraded { self?.updateTabUpgradedToUrl(url) }
- self?.openNewTabWithUrl(url, userEnteredValue: userEnteredValue, selected: selected, suggestion: suggestion)
+ if selected {
+ // reset address bar value
+ updateValue(selectedTabViewModel: nil, addressBarString: nil)
+ window?.makeFirstResponder(nil)
+ }
+ switch tabOrWindow {
+ case .tab:
+ tabCollectionViewModel.append(tab: tab, selected: selected)
+ case .window:
+ WindowsManager.openNewWindow(with: tab, showWindow: selected, popUp: false)
+ }
}
}
@@ -1122,10 +1135,6 @@ extension AddressBarTextField: SuggestionViewControllerDelegate {
func suggestionViewControllerDidConfirmSelection(_ suggestionViewController: SuggestionViewController) {
let suggestion = suggestionContainerViewModel?.selectedSuggestionViewModel?.suggestion
- if NSApp.isCommandPressed {
- openNewTab(selected: NSApp.isShiftPressed, suggestion: suggestion)
- return
- }
navigate(suggestion: suggestion)
}
diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift
index 214ef9cb75..8243d4ee8b 100644
--- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift
+++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift
@@ -122,10 +122,14 @@ final class MoreOptionsMenu: NSMenu {
private func setupMenuItems() {
#if FEEDBACK
- let feedbackMenuItem = NSMenuItem(title: UserText.sendFeedback, action: nil, keyEquivalent: "")
-#if !APPSTORE
- .withImage(NSImage(named: "BetaLabel"))
-#endif // !APPSTORE
+ let feedbackString: String = {
+ guard internalUserDecider.isInternalUser else {
+ return UserText.sendFeedback
+ }
+ return "\(UserText.sendFeedback) (version: \(AppVersion.shared.versionNumber).\(AppVersion.shared.buildNumber))"
+ }()
+ let feedbackMenuItem = NSMenuItem(title: feedbackString, action: nil, keyEquivalent: "")
+
feedbackMenuItem.submenu = FeedbackSubMenu(targetting: self, tabCollectionViewModel: tabCollectionViewModel)
addItem(feedbackMenuItem)
diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift
index eed2a83f4a..cfcc7cf553 100644
--- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift
+++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift
@@ -670,25 +670,26 @@ final class NavigationBarViewController: NSViewController {
downloadListCoordinator.updates
.throttle(for: 1.0, scheduler: DispatchQueue.main, latest: true)
.sink { [weak self] update in
- guard let self = self else { return }
+ guard let self else { return }
let shouldShowPopover = update.kind == .updated
+ && DownloadsPreferences().shouldOpenPopupOnCompletion
&& update.item.destinationURL != nil
&& update.item.tempURL == nil
&& !update.item.isBurner
- && WindowControllersManager.shared.lastKeyMainWindowController?.window === self.downloadsButton.window
+ && WindowControllersManager.shared.lastKeyMainWindowController?.window === downloadsButton.window
if shouldShowPopover {
- self.popovers.showDownloadsPopoverAndAutoHide(usingView: self.downloadsButton,
+ self.popovers.showDownloadsPopoverAndAutoHide(usingView: downloadsButton,
popoverDelegate: self,
downloadsDelegate: self)
} else {
if update.item.isBurner {
- self.invalidateDownloadButtonHidingTimer()
- self.updateDownloadsButton(updatingFromPinnedViewsNotification: false)
+ invalidateDownloadButtonHidingTimer()
+ updateDownloadsButton(updatingFromPinnedViewsNotification: false)
}
}
- self.updateDownloadsButton()
+ updateDownloadsButton()
}
.store(in: &downloadsCancellables)
downloadListCoordinator.progress
diff --git a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift
index ebf24cfbfb..c6b005c2fa 100644
--- a/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift
+++ b/DuckDuckGo/NavigationBar/View/NavigationButtonMenuDelegate.swift
@@ -19,6 +19,7 @@
import Cocoa
import Common
import WebKit
+import History
@MainActor
final class NavigationButtonMenuDelegate: NSObject {
diff --git a/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift b/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift
index 2f3358c167..3b61528b94 100644
--- a/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift
+++ b/DuckDuckGo/NavigationBar/ViewModel/BackForwardListItemViewModel.swift
@@ -17,6 +17,7 @@
//
import Cocoa
+import History
final class BackForwardListItemViewModel {
diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift
index 60b9120fb0..b363c3d843 100644
--- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift
+++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/EventMapping+NetworkProtectionError.swift
@@ -98,6 +98,9 @@ extension EventMapping where Event == NetworkProtectionError {
domainEvent = .networkProtectionUnhandledError(function: function, line: line, error: error)
frequency = .standard
return
+ case .vpnAccessRevoked:
+ // todo
+ return
}
let debugEvent = DebugEvent(eventType: .custom(domainEvent))
diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift
index a73cd66931..64e3aa7c3b 100644
--- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift
+++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/Invite/NetworkProtectionInviteDialog.swift
@@ -37,10 +37,4 @@ struct NetworkProtectionInviteDialog: View {
}
}
-struct NetworkProtectionInviteDialog_Previews: PreviewProvider {
- static var previews: some View {
- NetworkProtectionInviteDialog(model: NetworkProtectionInviteViewModel(delegate: NetworkProtectionInvitePresenter(), redemptionCoordinator: NetworkProtectionCodeRedemptionCoordinator()))
- }
-}
-
#endif
diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift
index 0838bb0c2d..a32ca7ee85 100644
--- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift
+++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtection+ConvenienceInitializers.swift
@@ -28,7 +28,11 @@ extension NetworkProtectionDeviceManager {
let settings = VPNSettings(defaults: .netP)
let keyStore = NetworkProtectionKeychainKeyStore()
let tokenStore = NetworkProtectionKeychainTokenStore()
- return NetworkProtectionDeviceManager(environment: settings.selectedEnvironment, tokenStore: tokenStore, keyStore: keyStore, errorEvents: .networkProtectionAppDebugEvents)
+ return NetworkProtectionDeviceManager(environment: settings.selectedEnvironment,
+ tokenStore: tokenStore,
+ keyStore: keyStore,
+ errorEvents: .networkProtectionAppDebugEvents,
+ isSubscriptionEnabled: false)
}
}
@@ -37,14 +41,16 @@ extension NetworkProtectionCodeRedemptionCoordinator {
let settings = VPNSettings(defaults: .netP)
self.init(environment: settings.selectedEnvironment,
tokenStore: NetworkProtectionKeychainTokenStore(),
- errorEvents: .networkProtectionAppDebugEvents)
+ errorEvents: .networkProtectionAppDebugEvents,
+ isSubscriptionEnabled: false)
}
}
extension NetworkProtectionKeychainTokenStore {
convenience init() {
self.init(keychainType: .default,
- errorEvents: .networkProtectionAppDebugEvents)
+ errorEvents: .networkProtectionAppDebugEvents,
+ isSubscriptionEnabled: false)
}
}
diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift
index e86a508f2d..5d658849b3 100644
--- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift
+++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/NetworkProtectionTunnelController.swift
@@ -242,6 +242,9 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr
.setDisableRekeying:
// Intentional no-op as this is handled by the extension or the agent's app delegate
break
+ case .setShowEntitlementAlert, .setShowEntitlementNotification:
+ // todo
+ break
}
}
@@ -284,6 +287,20 @@ final class NetworkProtectionTunnelController: TunnelController, TunnelSessionPr
}
}
+ // MARK: - Debug Command support
+
+ func relay(_ command: DebugCommand) async throws {
+ guard await isConnected,
+ let session = await session else {
+ return
+ }
+
+ let errorMessage: ExtensionMessageString? = try await session.sendProviderRequest(.debugCommand(command))
+ if let errorMessage {
+ throw TunnelFailureError(errorDescription: errorMessage.value)
+ }
+ }
+
// MARK: - Tunnel Configuration
/// Setups the tunnel manager if it's not set up already.
diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift
index 4279088dce..1cf5878f3b 100644
--- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift
+++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationPreferenceItem.swift
@@ -59,7 +59,6 @@ struct VPNLocationPreferenceItem: View {
.frame(height: 52)
.padding(.horizontal, 10)
.background(Color("BlackWhite1"))
- .animation(.default)
.roundedBorder()
}
diff --git a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift
index bc0a0e042f..f77d8fd032 100644
--- a/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift
+++ b/DuckDuckGo/NetworkProtection/AppTargets/BothAppTargets/VPNLocation/VPNLocationViewModel.swift
@@ -191,7 +191,8 @@ extension NetworkProtectionLocationListCompositeRepository {
self.init(
environment: settings.selectedEnvironment,
tokenStore: NetworkProtectionKeychainTokenStore(),
- errorEvents: .networkProtectionAppDebugEvents
+ errorEvents: .networkProtectionAppDebugEvents,
+ isSubscriptionEnabled: false
)
}
}
diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift
index 4237c816d2..3f8776fab2 100644
--- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift
+++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/AppExtensionAndNotificationTargets/NetworkProtectionUNNotificationsPresenter.swift
@@ -140,6 +140,10 @@ final class NetworkProtectionUNNotificationsPresenter: NSObject, NetworkProtecti
showNotification(.test, content)
}
+ func showEntitlementNotification(completion: @escaping (Error?) -> Void) {
+ // todo
+ }
+
private func showNotification(_ identifier: NetworkProtectionNotificationIdentifier, _ content: UNNotificationContent) {
let request = UNNotificationRequest(identifier: identifier.rawValue, content: content, trigger: .none)
diff --git a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift
index af8f42cb74..ee8e936733 100644
--- a/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift
+++ b/DuckDuckGo/NetworkProtection/NetworkExtensionTargets/NetworkExtensionTargets/MacPacketTunnelProvider.swift
@@ -123,6 +123,9 @@ final class MacPacketTunnelProvider: PacketTunnelProvider {
.failedToParseLocationListResponse:
// Needs Privacy triage for macOS Geoswitching pixels
return
+ case .vpnAccessRevoked:
+ // todo
+ return
}
PixelKit.fire(domainEvent, frequency: .dailyAndContinuous, includeAppVersionParameter: true)
@@ -226,7 +229,8 @@ final class MacPacketTunnelProvider: PacketTunnelProvider {
let debugEvents = Self.networkProtectionDebugEvents(controllerErrorStore: controllerErrorStore)
let tokenStore = NetworkProtectionKeychainTokenStore(keychainType: Bundle.keychainType,
serviceName: Self.tokenServiceName,
- errorEvents: debugEvents)
+ errorEvents: debugEvents,
+ isSubscriptionEnabled: false)
let notificationsPresenter = NetworkProtectionNotificationsPresenterFactory().make(settings: settings)
super.init(notificationsPresenter: notificationsPresenter,
@@ -236,7 +240,9 @@ final class MacPacketTunnelProvider: PacketTunnelProvider {
tokenStore: tokenStore,
debugEvents: debugEvents,
providerEvents: Self.packetTunnelProviderEvents,
- settings: settings)
+ settings: settings,
+ isSubscriptionEnabled: false,
+ entitlementCheck: nil)
observeServerChanges()
observeStatusUpdateRequests()
diff --git a/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift b/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift
index 5786b56e9e..e9448763fc 100644
--- a/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift
+++ b/DuckDuckGo/Onboarding/ViewModel/OnboardingViewModel.swift
@@ -53,7 +53,30 @@ final class OnboardingViewModel: ObservableObject {
}
@UserDefaultsWrapper(key: .onboardingFinished, defaultValue: false)
- private(set) static var isOnboardingFinished: Bool
+ private static var _isOnboardingFinished: Bool
+
+ @MainActor
+ private(set) static var isOnboardingFinished: Bool {
+ get {
+ guard !_isOnboardingFinished else { return true }
+
+ // when thereβs a restored state but Onboarding Finished flag is not set - set it
+ guard WindowsManager.mainWindows.count <= 1 else {
+ OnboardingViewModel.isOnboardingFinished = true
+ return true
+ }
+ guard let tabsContent = (WindowsManager.mainWindows.first?.contentViewController as? MainViewController)?.tabCollectionViewModel.tabs.map(\.content) else { return false }
+ if !tabsContent.isEmpty, tabsContent != [.newtab] {
+ // thereβs some tabs content not equal to the new tab page: it means thereβs a session restored
+ OnboardingViewModel.isOnboardingFinished = true
+ return true
+ }
+ return false
+ }
+ set {
+ _isOnboardingFinished = newValue
+ }
+ }
weak var delegate: OnboardingDelegate?
@@ -79,6 +102,7 @@ final class OnboardingViewModel: ObservableObject {
state = .setDefault
}
+ @MainActor
func onSetDefaultPressed() {
delegate?.onboardingDidRequestSetDefault { [weak self] in
self?.state = .startBrowsing
@@ -87,6 +111,7 @@ final class OnboardingViewModel: ObservableObject {
}
}
+ @MainActor
func onSetDefaultSkipped() {
state = .startBrowsing
Self.isOnboardingFinished = true
@@ -97,6 +122,7 @@ final class OnboardingViewModel: ObservableObject {
skipTypingRequested = true
}
+ @MainActor
func onboardingReshown() {
if Self.isOnboardingFinished {
typingDisabled = true
diff --git a/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift b/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift
index f3c812e4fc..153f3afab9 100644
--- a/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift
+++ b/DuckDuckGo/Preferences/Model/DownloadsPreferences.swift
@@ -23,6 +23,7 @@ protocol DownloadsPreferencesPersistor {
var lastUsedCustomDownloadLocation: String? { get set }
var alwaysRequestDownloadLocation: Bool { get set }
+ var shouldOpenPopupOnCompletion: Bool { get set }
var defaultDownloadLocation: URL? { get }
func isDownloadLocationValid(_ location: URL) -> Bool
@@ -38,6 +39,9 @@ struct DownloadsPreferencesUserDefaultsPersistor: DownloadsPreferencesPersistor
@UserDefaultsWrapper(key: .alwaysRequestDownloadLocationKey, defaultValue: false)
var alwaysRequestDownloadLocation: Bool
+ @UserDefaultsWrapper(key: .openDownloadsPopupOnCompletionKey, defaultValue: true)
+ var shouldOpenPopupOnCompletion: Bool
+
var defaultDownloadLocation: URL? {
let fileManager = FileManager.default
let folders = fileManager.urls(for: .downloadsDirectory, in: .userDomainMask)
@@ -119,13 +123,22 @@ final class DownloadsPreferences: ObservableObject {
get {
persistor.alwaysRequestDownloadLocation
}
-
set {
persistor.alwaysRequestDownloadLocation = newValue
objectWillChange.send()
}
}
+ var shouldOpenPopupOnCompletion: Bool {
+ get {
+ persistor.shouldOpenPopupOnCompletion
+ }
+ set {
+ persistor.shouldOpenPopupOnCompletion = newValue
+ objectWillChange.send()
+ }
+ }
+
func presentDownloadDirectoryPanel() {
let panel = NSOpenPanel.downloadDirectoryPanel()
let result = panel.runModal()
diff --git a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift
index 96315f1cc0..d5b5cbae02 100644
--- a/DuckDuckGo/Preferences/View/PreferencesAboutView.swift
+++ b/DuckDuckGo/Preferences/View/PreferencesAboutView.swift
@@ -54,7 +54,7 @@ extension Preferences {
Text(UserText.versionLabel(version: model.appVersion.versionNumber, build: model.appVersion.buildNumber))
.onTapGesture(count: 12) {
-#if NETWORK_PROTECTION && !SUBSCRIPTION
+#if NETWORK_PROTECTION
model.displayNetPInvite()
#endif
}
diff --git a/DuckDuckGo/Preferences/View/PreferencesDownloadsView.swift b/DuckDuckGo/Preferences/View/PreferencesDownloadsView.swift
index 4dd923a87d..1463b5fbff 100644
--- a/DuckDuckGo/Preferences/View/PreferencesDownloadsView.swift
+++ b/DuckDuckGo/Preferences/View/PreferencesDownloadsView.swift
@@ -28,7 +28,12 @@ extension Preferences {
var body: some View {
PreferencePane(UserText.downloads) {
- // SECTION 1: Location
+ PreferencePaneSubSection {
+ ToggleMenuItem(UserText.downloadsOpenPopupOnCompletion,
+ isOn: $model.shouldOpenPopupOnCompletion)
+ }
+
+ // MARK: Location
PreferencePaneSection(UserText.downloadsLocation) {
HStack {
@@ -40,9 +45,21 @@ extension Preferences {
#endif
}
.disabled(model.alwaysRequestDownloadLocation)
- ToggleMenuItem(UserText.downloadsAlwaysAsk, isOn: $model.alwaysRequestDownloadLocation)
+ ToggleMenuItem(UserText.downloadsAlwaysAsk,
+ isOn: $model.alwaysRequestDownloadLocation)
}
}
}
}
}
+
+#Preview {
+ VStack {
+ HStack {
+ Preferences.DownloadsView(model: DownloadsPreferences())
+ .padding()
+ Spacer()
+ }.frame(width: 500)
+
+ }.background(Color.preferencesBackground)
+}
diff --git a/DuckDuckGo/Preferences/View/PreferencesSidebar.swift b/DuckDuckGo/Preferences/View/PreferencesSidebar.swift
index b948508d61..8a78d016cb 100644
--- a/DuckDuckGo/Preferences/View/PreferencesSidebar.swift
+++ b/DuckDuckGo/Preferences/View/PreferencesSidebar.swift
@@ -77,16 +77,7 @@ extension Preferences {
ScrollView {
VStack(spacing: 0) {
ForEach(model.sections) { section in
- ForEach(section.panes) { pane in
- SidebarItem(pane: pane, isSelected: model.selectedPane == pane) {
- model.selectPane(pane)
- }
- }
- if section != model.sections.last {
- Color(NSColor.separatorColor)
- .frame(height: 1)
- .padding(8)
- }
+ sidebarSection(section)
}
}
}
@@ -95,6 +86,20 @@ extension Preferences {
.padding(.top, 18)
.padding(.horizontal, 20)
}
+
+ @ViewBuilder
+ private func sidebarSection(_ section: PreferencesSection) -> some View {
+ ForEach(section.panes) { pane in
+ SidebarItem(pane: pane, isSelected: model.selectedPane == pane) {
+ model.selectPane(pane)
+ }
+ }
+ if section != model.sections.last {
+ Color(NSColor.separatorColor)
+ .frame(height: 1)
+ .padding(8)
+ }
+ }
}
private struct SidebarItemButtonStyle: ButtonStyle {
diff --git a/DuckDuckGo/SecureVault/Model/PasswordManagementListSection.swift b/DuckDuckGo/SecureVault/Model/PasswordManagementListSection.swift
index f2fa429d90..f251519bf5 100644
--- a/DuckDuckGo/SecureVault/Model/PasswordManagementListSection.swift
+++ b/DuckDuckGo/SecureVault/Model/PasswordManagementListSection.swift
@@ -114,13 +114,19 @@ struct PasswordManagementListSection {
return DateMetadata(title: Self.dateFormatter.string(from: date), month: month, year: year)
}
- let metadataSortFunction: (DateMetadata, DateMetadata) -> Bool = order == .ascending ? (>) : (<)
- let dateSortFunction: (Date, Date) -> Bool = order == .ascending ? (>) : (<)
- let sortedKeys = itemsByDateMetadata.keys.sorted(by: metadataSortFunction)
+ let sortedKeys = switch order {
+ case .ascending: itemsByDateMetadata.keys.sorted(by: (>))
+ case .descending: itemsByDateMetadata.keys.sorted(by: (<))
+ }
return sortedKeys.map { key in
- var itemsInSection = itemsByDateMetadata[key] ?? []
- itemsInSection.sort { lhs, rhs in dateSortFunction(lhs[keyPath: keyPath], rhs[keyPath: keyPath]) }
+ var itemsInSection = itemsByDateMetadata[key, default: []]
+ switch order {
+ case .ascending:
+ itemsInSection.sort(by: { $0[keyPath: keyPath] > $1[keyPath: keyPath] })
+ case .descending:
+ itemsInSection.sort(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] })
+ }
return PasswordManagementListSection(title: key.title, items: itemsInSection)
}
}
diff --git a/DuckDuckGo/Sharing/SharingMenu.swift b/DuckDuckGo/Sharing/SharingMenu.swift
index 50c5808e4a..992ddb6103 100644
--- a/DuckDuckGo/Sharing/SharingMenu.swift
+++ b/DuckDuckGo/Sharing/SharingMenu.swift
@@ -72,13 +72,12 @@ final class SharingMenu: NSMenu {
let data = try? PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0)
let descriptor = NSAppleEventDescriptor(descriptorType: .openSharingSubpane, data: data)
- // if you want to refactor this to remove the warning - stop now
- // the suggested method with NSWorkspace.OpenConfiguration doesnβt work for Sharing Preferences
- NSWorkspace.shared.open([url],
- withAppBundleIdentifier: nil,
- options: .async,
- additionalEventParamDescriptor: descriptor,
- launchIdentifiers: nil)
+ // the non-deprecated method with NSWorkspace.OpenConfiguration doesnβt work for Sharing Preferences
+ (NSWorkspace.shared as Workspace).open([url],
+ withAppBundleIdentifier: nil,
+ options: [],
+ additionalEventParamDescriptor: descriptor,
+ launchIdentifiers: nil)
}
@objc func sharingItemSelected(_ sender: NSMenuItem) {
diff --git a/DuckDuckGo/StateRestoration/AppStateRestorationManager.swift b/DuckDuckGo/StateRestoration/AppStateRestorationManager.swift
index aa90d12f67..521b01b490 100644
--- a/DuckDuckGo/StateRestoration/AppStateRestorationManager.swift
+++ b/DuckDuckGo/StateRestoration/AppStateRestorationManager.swift
@@ -58,6 +58,7 @@ final class AppStateRestorationManager: NSObject {
service.canRestoreLastSessionState
}
+ @discardableResult
func restoreLastSessionState(interactive: Bool) -> WindowManagerStateRestoration? {
var state: WindowManagerStateRestoration?
do {
diff --git a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift
index ff2eb60258..d1be46c375 100644
--- a/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift
+++ b/DuckDuckGo/Suggestions/Model/SuggestionContainer.swift
@@ -19,6 +19,7 @@
import Foundation
import BrowserServicesKit
import Common
+import History
final class SuggestionContainer {
@@ -85,7 +86,7 @@ final class SuggestionContainer {
extension SuggestionContainer: SuggestionLoadingDataSource {
- func history(for suggestionLoading: SuggestionLoading) -> [BrowserServicesKit.HistoryEntry] {
+ func history(for suggestionLoading: SuggestionLoading) -> [BrowserServicesKit.HistorySuggestion] {
return historyCoordinating.history ?? []
}
@@ -108,9 +109,9 @@ extension SuggestionContainer: SuggestionLoadingDataSource {
}
-extension HistoryEntry: BrowserServicesKit.HistoryEntry {
+extension HistoryEntry: HistorySuggestion {
- var numberOfVisits: Int {
+ public var numberOfVisits: Int {
return numberOfTotalVisits
}
diff --git a/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift b/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift
index 97133045a0..3de0df0c9b 100644
--- a/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift
+++ b/DuckDuckGo/Tab/Model/Tab+UIDelegate.swift
@@ -22,6 +22,7 @@ import Foundation
import Navigation
import UniformTypeIdentifiers
import WebKit
+import PDFKit
extension Tab: WKUIDelegate, PrintingUserScriptDelegate {
@@ -35,9 +36,22 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate {
self.value(forKey: Tab.objcNewWindowPolicyDecisionMakersKeyPath) as? [NewWindowPolicyDecisionMaker]
}
+ @MainActor private static var expectedSaveDataToFileCallback: (@MainActor (URL?) -> Void)?
+ @MainActor
+ private static func consumeExpectedSaveDataToFileCallback() -> (@MainActor (URL?) -> Void)? {
+ defer {
+ expectedSaveDataToFileCallback = nil
+ }
+ return expectedSaveDataToFileCallback
+ }
+
@objc(_webView:saveDataToFile:suggestedFilename:mimeType:originatingURL:)
func webView(_ webView: WKWebView, saveDataToFile data: Data, suggestedFilename: String, mimeType: String, originatingURL: URL) {
- saveDownloaded(data: data, suggestedFilename: suggestedFilename, mimeType: mimeType)
+ Task {
+ let result = try? await saveDownloadedData(data, suggestedFilename: suggestedFilename, mimeType: mimeType, originatingURL: originatingURL)
+ // when print function saves a PDF setting the callback, return the saved temporary file to it
+ await Self.consumeExpectedSaveDataToFileCallback()?(result)
+ }
}
@MainActor
@@ -299,6 +313,10 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate {
printOperation.view?.frame = webView.bounds
}
+ runPrintOperation(printOperation, completionHandler: completionHandler)
+ }
+
+ func runPrintOperation(_ printOperation: NSPrintOperation, completionHandler: ((Bool) -> Void)? = nil) {
let dialog = UserDialogType.print(.init(printOperation) { result in
completionHandler?((try? result.get()) ?? false)
})
@@ -315,7 +333,29 @@ extension Tab: WKUIDelegate, PrintingUserScriptDelegate {
self.runPrintOperation(for: frameHandle, in: webView) { _ in completionHandler() }
}
- func print() {
+ @MainActor(unsafe)
+ func print(pdfHUD: WKPDFHUDViewWrapper? = nil) {
+ if let pdfHUD {
+ Self.expectedSaveDataToFileCallback = { [weak self] url in
+ guard let self, let url,
+ let pdfDocument = PDFDocument(url: url) else {
+ assertionFailure("Could not load PDF document from \(url?.path ?? "")")
+ return
+ }
+ // Set up NSPrintOperation
+ guard let printOperation = pdfDocument.printOperation(for: .shared, scalingMode: .pageScaleNone, autoRotate: false) else {
+ assertionFailure("Could not print PDF document")
+ return
+ }
+
+ self.runPrintOperation(printOperation) { _ in
+ try? FileManager.default.removeItem(at: url)
+ }
+ }
+ saveWebContent(pdfHUD: pdfHUD, location: .temporary)
+ return
+ }
+
self.runPrintOperation(for: nil, in: self.webView)
}
diff --git a/DuckDuckGo/Tab/Model/Tab.swift b/DuckDuckGo/Tab/Model/Tab.swift
index 297e4d55eb..09fc3be7a4 100644
--- a/DuckDuckGo/Tab/Model/Tab.swift
+++ b/DuckDuckGo/Tab/Model/Tab.swift
@@ -24,6 +24,7 @@ import Foundation
import Navigation
import UserScript
import WebKit
+import History
#if SUBSCRIPTION
import Subscription
@@ -66,7 +67,7 @@ protocol NewWindowPolicyDecisionMaker {
enum URLSource: Equatable {
case pendingStateRestoration
case loadedByStateRestoration
- case userEntered(String)
+ case userEntered(String, downloadRequested: Bool = false)
case historyEntry
case bookmark
case ui
@@ -77,7 +78,7 @@ protocol NewWindowPolicyDecisionMaker {
case webViewUpdated
var userEnteredValue: String? {
- if case .userEntered(let userEnteredValue) = self {
+ if case .userEntered(let userEnteredValue, _) = self {
userEnteredValue
} else {
nil
@@ -90,6 +91,8 @@ protocol NewWindowPolicyDecisionMaker {
var navigationType: NavigationType {
switch self {
+ case .userEntered(_, downloadRequested: true):
+ .custom(.userRequestedPageDownload)
case .userEntered:
.custom(.userEnteredUrl)
case .pendingStateRestoration:
@@ -259,6 +262,14 @@ protocol NewWindowPolicyDecisionMaker {
userEnteredValue != nil
}
+ var isUserRequestedPageDownload: Bool {
+ if case .url(_, credential: _, source: .userEntered(_, downloadRequested: true)) = self {
+ return true
+ } else {
+ return false
+ }
+ }
+
var displaysContentInWebView: Bool {
isUrl
}
@@ -1009,7 +1020,7 @@ protocol NewWindowPolicyDecisionMaker {
return nil
}
- if webView.url == url, webView.backForwardList.currentItem?.url == url, !webView.isLoading {
+ if webView.url == url, webView.backForwardList.currentItem?.url == url, !webView.isLoading, !content.isUserRequestedPageDownload {
return reload()
}
if restoreInteractionStateIfNeeded() { return nil /* session restored */ }
diff --git a/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift b/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift
index 713f737e12..52b1021406 100644
--- a/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift
+++ b/DuckDuckGo/Tab/Navigation/SearchNonexistentDomainNavigationResponder.swift
@@ -33,7 +33,7 @@ final class SearchNonexistentDomainNavigationResponder {
self.setContent = setContent
cancellable = contentPublisher.sink { [weak self] tabContent in
- if case .url(_, credential: .none, source: .userEntered(let userEnteredValue)) = tabContent {
+ if case .url(_, credential: .none, source: .userEntered(let userEnteredValue, _)) = tabContent {
self?.lastUserEnteredValue = userEnteredValue
}
}
diff --git a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift
index 79ce4aed1d..c8a3057d70 100644
--- a/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift
+++ b/DuckDuckGo/Tab/TabExtensions/DownloadsTabExtension.swift
@@ -34,10 +34,17 @@ final class DownloadsTabExtension: NSObject {
private let isBurner: Bool
private var isRestoringSessionState = false
+ enum DownloadLocation {
+ case auto
+ case prompt
+ case temporary
+ }
+ private var nextSaveDataRequestDownloadLocation: DownloadLocation = .auto
+
@Published
private(set) var savePanelDialogRequest: SavePanelDialogRequest? {
- didSet {
- savePanelDialogRequest?.addCompletionHandler { [weak self, weak savePanelDialogRequest] _ in
+ willSet {
+ newValue?.addCompletionHandler { [weak self, weak savePanelDialogRequest=newValue] _ in
if let self,
let savePanelDialogRequest,
self.savePanelDialogRequest === savePanelDialogRequest {
@@ -57,44 +64,88 @@ final class DownloadsTabExtension: NSObject {
super.init()
}
- func saveWebViewContentAs(_ webView: WKWebView) {
+ func saveWebViewContent(from webView: WKWebView, pdfHUD: WKPDFHUDViewWrapper?, location: DownloadLocation) {
Task { @MainActor in
- await saveWebViewContentAs(webView)
+ await saveWebViewContent(from: webView, pdfHUD: pdfHUD, location: location)
}
}
@MainActor
- private func saveWebViewContentAs(_ webView: WKWebView) async {
- guard await webView.mimeType == UTType.html.preferredMIMEType else {
- if let url = webView.url {
- webView.startDownload(using: URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad)) { download in
- self.downloadManager.add(download,
- fromBurnerWindow: self.isBurner,
- delegate: self, location: .prompt)
- }
+ private func saveWebViewContent(from webView: WKWebView, pdfHUD: WKPDFHUDViewWrapper?, location: DownloadLocation) async {
+ let mimeType = pdfHUD != nil ? UTType.pdf.preferredMIMEType : await webView.mimeType
+ switch mimeType {
+ case UTType.html.preferredMIMEType:
+ assert([.prompt, .auto].contains(location))
+
+ let parameters = SavePanelParameters(suggestedFilename: webView.suggestedFilename, fileTypes: [.html, .webArchive, .pdf])
+ self.savePanelDialogRequest = SavePanelDialogRequest(parameters) { result in
+ guard let (url, fileType) = try? result.get() else { return }
+ webView.exportWebContent(to: url, as: fileType.flatMap(WKWebView.ContentExportType.init) ?? .html)
}
- return
+
+ case UTType.pdf.preferredMIMEType:
+ self.nextSaveDataRequestDownloadLocation = location
+ let success = webView.savePDF(pdfHUD) // calls `saveDownloadedData(_:suggestedFilename:mimeType:originatingURL)`
+ guard success else { fallthrough }
+
+ default:
+ guard let url = webView.url else {
+ assertionFailure("Canβt save web content without URL loaded")
+ return
+ }
+ if url.isFileURL {
+ self.nextSaveDataRequestDownloadLocation = location
+ _=try? await self.saveDownloadedData(nil, suggestedFilename: url.lastPathComponent, mimeType: mimeType ?? "text/html", originatingURL: url)
+ return
+ }
+
+ let download = await webView.startDownload(using: URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad))
+
+ let location = self.downloadLocation(for: location, suggestedFilename: download.webView?.suggestedFilename ?? "")
+ self.downloadManager.add(download,
+ fromBurnerWindow: self.isBurner,
+ delegate: self,
+ location: location)
}
- let parameters = SavePanelParameters(suggestedFilename: webView.suggestedFilename, fileTypes: [.html, .webArchive, .pdf])
- self.savePanelDialogRequest = SavePanelDialogRequest(parameters) { result in
- guard let (url, fileType) = try? result.get() else { return }
- webView.exportWebContent(to: url, as: fileType.flatMap(WKWebView.ContentExportType.init) ?? .html)
+ }
+
+ private func downloadLocation(for location: DownloadLocation, suggestedFilename: String) -> FileDownloadManager.DownloadLocationPreference {
+ switch location {
+ case .auto:
+ return .auto
+ case .prompt:
+ return .prompt
+ case .temporary:
+ let suggestedFilename = suggestedFilename.isEmpty ? UUID().uuidString : suggestedFilename
+ let fm = FileManager.default
+ let dirURL = fm.temporaryDirectory.appendingPathComponent(.uniqueFilename())
+ try? fm.createDirectory(at: dirURL, withIntermediateDirectories: true)
+ return .preset(destinationURL: dirURL.appendingPathComponent(suggestedFilename), tempURL: nil)
}
}
- private func saveDownloaded(data: Data, to toURL: URL) {
+ private func saveDownloadedData(_ data: Data?, to toURL: URL, originatingURL: URL) throws {
let fm = FileManager.default
- let tempURL = fm.temporaryDirectory.appendingPathComponent(.uniqueFilename())
- do {
- // First save file in a temporary directory
- try data.write(to: tempURL)
- // Then move the file to the download location and show a bounce if the file is in a location on the user's dock.
+
+ // if no data provided - copy file from local url to the destination url
+ guard let data else {
+ guard originatingURL.isFileURL else {
+ assertionFailure("No data provided for non-file URL")
+ return
+ }
try Progress.withPublishedProgress(url: toURL) {
- _ = try fm.moveItem(at: tempURL, to: toURL, incrementingIndexIfExists: true)
+ try fm.copyItem(at: originatingURL, to: toURL, incrementingIndexIfExists: true)
}
- } catch {
- os_log("Failed to save PDF file to Downloads folder", type: .error)
+ return
+ }
+
+ let tempURL = fm.temporaryDirectory.appendingPathComponent(.uniqueFilename())
+ // First save file in a temporary directory
+ try data.write(to: tempURL)
+ // Then move the file to the download location and show a bounce if the file is in a location on the user's dock.
+ try Progress.withPublishedProgress(url: toURL) {
+ try fm.moveItem(at: tempURL, to: toURL, incrementingIndexIfExists: true)
}
}
@@ -129,8 +180,11 @@ extension DownloadsTabExtension: NavigationResponder {
let firstNavigationAction = navigationResponse.mainFrameNavigation?.redirectHistory.first
?? navigationResponse.mainFrameNavigation?.navigationAction
- guard navigationResponse.httpResponse?.isSuccessful == true,
- !navigationResponse.canShowMIMEType || navigationResponse.shouldDownload else {
+ guard navigationResponse.httpResponse?.isSuccessful != false, // download non-http responses
+ !navigationResponse.canShowMIMEType || navigationResponse.shouldDownload
+ // if user pressed Opt+Enter in the Address bar to download from a URL
+ || (navigationResponse.mainFrameNavigation?.redirectHistory.last ?? navigationResponse.mainFrameNavigation?.navigationAction)?.navigationType == .custom(.userRequestedPageDownload)
+ else {
return .next // proceed with normal page loading
}
@@ -206,8 +260,7 @@ extension DownloadsTabExtension: DownloadTaskDelegate {
@MainActor
func chooseDestination(suggestedFilename: String?, directoryURL: URL?, fileTypes: [UTType], callback: @escaping @MainActor (URL?, UTType?) -> Void) {
- savePanelDialogRequest = SavePanelDialogRequest(SavePanelParameters(suggestedFilename: suggestedFilename, fileTypes: fileTypes)) { [weak self] result in
- self?.savePanelDialogRequest = nil
+ savePanelDialogRequest = SavePanelDialogRequest(SavePanelParameters(suggestedFilename: suggestedFilename, fileTypes: fileTypes)) { result in
guard case let .success(.some( (url: url, fileType: fileType) )) = result else {
callback(nil, nil)
return
@@ -226,9 +279,9 @@ protocol DownloadsTabExtensionProtocol: AnyObject, NavigationResponder, Download
var delegate: TabDownloadsDelegate? { get set }
var savePanelDialogPublisher: AnyPublisher { get }
- func saveWebViewContentAs(_ webView: WKWebView)
+ func saveWebViewContent(from webView: WKWebView, pdfHUD: WKPDFHUDViewWrapper?, location: DownloadsTabExtension.DownloadLocation)
- func saveDownloaded(data: Data, suggestedFilename: String, mimeType: String)
+ func saveDownloadedData(_ data: Data?, suggestedFilename: String, mimeType: String, originatingURL: URL) async throws -> URL?
}
extension DownloadsTabExtension: TabExtension, DownloadsTabExtensionProtocol {
@@ -241,19 +294,35 @@ extension DownloadsTabExtension: TabExtension, DownloadsTabExtensionProtocol {
}
@MainActor
- func saveDownloaded(data: Data, suggestedFilename: String, mimeType: String) {
- if !downloadsPreferences.alwaysRequestDownloadLocation,
- let location = downloadsPreferences.effectiveDownloadLocation {
- let url = location.appendingPathComponent(suggestedFilename)
- saveDownloaded(data: data, to: url)
- return
+ func saveDownloadedData(_ data: Data?, suggestedFilename: String, mimeType: String, originatingURL: URL) async throws -> URL? {
+ defer {
+ self.nextSaveDataRequestDownloadLocation = .auto
}
+ switch downloadLocation(for: nextSaveDataRequestDownloadLocation, suggestedFilename: suggestedFilename) {
+ case .auto:
+ guard !downloadsPreferences.alwaysRequestDownloadLocation,
+ let location = downloadsPreferences.effectiveDownloadLocation else { fallthrough /* prompt */ }
+
+ let url = location.appendingPathComponent(suggestedFilename)
+ try saveDownloadedData(data, to: url, originatingURL: originatingURL)
+ return url
+
+ case .prompt:
+ let fileTypes = UTType(mimeType: mimeType).map { [$0] } ?? []
+ let url: URL? = await withCheckedContinuation { continuation in
+ chooseDestination(suggestedFilename: suggestedFilename, directoryURL: nil, fileTypes: fileTypes) { url, _ in
+ continuation.resume(returning: url)
+ }
+ }
+
+ guard let url else { return nil }
- let fileTypes = UTType(mimeType: mimeType).map { [$0] } ?? []
- chooseDestination(suggestedFilename: suggestedFilename, directoryURL: nil, fileTypes: fileTypes) { [weak self] url, _ in
- guard let url else { return }
+ try saveDownloadedData(data, to: url, originatingURL: originatingURL)
+ return url
- self?.saveDownloaded(data: data, to: url)
+ case .preset(destinationURL: let destinationURL, tempURL: _):
+ try saveDownloadedData(data, to: destinationURL, originatingURL: originatingURL)
+ return destinationURL
}
}
}
@@ -266,12 +335,12 @@ extension TabExtensions {
extension Tab {
- func saveWebContentAs() {
- self.downloads?.saveWebViewContentAs(webView)
+ func saveWebContent(pdfHUD: WKPDFHUDViewWrapper?, location: DownloadsTabExtension.DownloadLocation) {
+ self.downloads?.saveWebViewContent(from: webView, pdfHUD: pdfHUD, location: location)
}
- func saveDownloaded(data: Data, suggestedFilename: String, mimeType: String) {
- self.downloads?.saveDownloaded(data: data, suggestedFilename: suggestedFilename, mimeType: mimeType)
+ func saveDownloadedData(_ data: Data, suggestedFilename: String, mimeType: String, originatingURL: URL) async throws -> URL? {
+ try await self.downloads?.saveDownloadedData(data, suggestedFilename: suggestedFilename, mimeType: mimeType, originatingURL: originatingURL)
}
}
diff --git a/DuckDuckGo/Tab/TabExtensions/HistoryTabExtension.swift b/DuckDuckGo/Tab/TabExtensions/HistoryTabExtension.swift
index 59ee066407..90a23daa73 100644
--- a/DuckDuckGo/Tab/TabExtensions/HistoryTabExtension.swift
+++ b/DuckDuckGo/Tab/TabExtensions/HistoryTabExtension.swift
@@ -21,6 +21,7 @@ import Common
import ContentBlocking
import Foundation
import Navigation
+import History
final class HistoryTabExtension: NSObject {
diff --git a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift
index dd6f107694..56351d7c11 100644
--- a/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift
+++ b/DuckDuckGo/Tab/TabExtensions/TabExtensions.swift
@@ -21,6 +21,7 @@ import Combine
import ContentBlocking
import Foundation
import PrivacyDashboard
+import History
/**
Tab Extensions should conform to TabExtension protocol
diff --git a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift
index 2c2ce64d58..1d2f33d91e 100644
--- a/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift
+++ b/DuckDuckGo/Tab/UserScripts/SubscriptionPagesUserScript.swift
@@ -119,7 +119,7 @@ final class SubscriptionPagesUseSubscriptionFeature: Subfeature {
}
func getSubscription(params: Any, original: WKScriptMessage) async throws -> Encodable? {
- if let authToken = AccountManager().authToken, let accessToken = AccountManager().accessToken {
+ if let authToken = AccountManager().authToken, AccountManager().accessToken != nil {
return Subscription(token: authToken)
} else {
return Subscription(token: "")
diff --git a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift
index 3f9cb377d9..7ed73a4504 100644
--- a/DuckDuckGo/Tab/ViewModel/TabViewModel.swift
+++ b/DuckDuckGo/Tab/ViewModel/TabViewModel.swift
@@ -112,6 +112,10 @@ final class TabViewModel {
// Update the address bar only after the tab did commit navigation to prevent Address Bar Spoofing
return tab.webViewDidCommitNavigationPublisher.map { .didCommit }.eraseToAnyPublisher()
+ case .url(_, _, source: .userEntered(_, downloadRequested: true)):
+ // donβt update the address bar for download navigations
+ return Empty().eraseToAnyPublisher().eraseToAnyPublisher()
+
case .url(_, _, source: .pendingStateRestoration),
.url(_, _, source: .loadedByStateRestoration),
.url(_, _, source: .userEntered),
diff --git a/DuckDuckGo/TabBar/Model/TabCollection.swift b/DuckDuckGo/TabBar/Model/TabCollection.swift
index 9bc2d27e74..94b632fa86 100644
--- a/DuckDuckGo/TabBar/Model/TabCollection.swift
+++ b/DuckDuckGo/TabBar/Model/TabCollection.swift
@@ -18,6 +18,7 @@
import Foundation
import Combine
+import History
final class TabCollection: NSObject {
diff --git a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift
index 92e3814677..e0319a25e8 100644
--- a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift
+++ b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift
@@ -19,6 +19,7 @@
import Common
import Foundation
import Combine
+import History
/**
* The delegate callbacks are triggered for events related to unpinned tabs only.
diff --git a/DuckDuckGo/Windows/View/WindowControllersManager.swift b/DuckDuckGo/Windows/View/WindowControllersManager.swift
index 7f1e0cda73..0aeaeeadd2 100644
--- a/DuckDuckGo/Windows/View/WindowControllersManager.swift
+++ b/DuckDuckGo/Windows/View/WindowControllersManager.swift
@@ -140,28 +140,6 @@ extension WindowControllersManager {
}
func show(url: URL?, source: Tab.TabContent.URLSource, newTab: Bool = false) {
-
- func show(url: URL?, in windowController: MainWindowController) {
- let viewController = windowController.mainViewController
- windowController.window?.makeKeyAndOrderFront(self)
-
- let tabCollectionViewModel = viewController.tabCollectionViewModel
- let tabCollection = tabCollectionViewModel.tabCollection
-
- if tabCollection.tabs.count == 1,
- let firstTab = tabCollection.tabs.first,
- case .newtab = firstTab.content,
- !newTab {
- firstTab.setContent(url.map { .url($0, source: source) } ?? .newtab)
- } else if let tab = tabCollectionViewModel.selectedTabViewModel?.tab, !newTab {
- tab.setContent(url.map { .url($0, source: source) } ?? .newtab)
- } else {
- let newTab = Tab(content: url.map { .url($0, source: source) } ?? .newtab, shouldLoadInBackground: true, burnerMode: tabCollectionViewModel.burnerMode)
- newTab.setContent(url.map { .url($0, source: source) } ?? .newtab)
- tabCollectionViewModel.append(tab: newTab)
- }
- }
-
let nonPopupMainWindowControllers = mainWindowControllers.filter { $0.window?.isPopUpWindow == false }
// If there is a main window, open the URL in it
@@ -173,7 +151,7 @@ extension WindowControllersManager {
// If there is any non-popup window available, open the URL in it
?? nonPopupMainWindowControllers.first {
- show(url: url, in: windowController)
+ show(url: url, in: windowController, source: source, newTab: newTab)
return
}
@@ -185,6 +163,27 @@ extension WindowControllersManager {
}
}
+ private func show(url: URL?, in windowController: MainWindowController, source: Tab.TabContent.URLSource, newTab: Bool) {
+ let viewController = windowController.mainViewController
+ windowController.window?.makeKeyAndOrderFront(self)
+
+ let tabCollectionViewModel = viewController.tabCollectionViewModel
+ let tabCollection = tabCollectionViewModel.tabCollection
+
+ if tabCollection.tabs.count == 1,
+ let firstTab = tabCollection.tabs.first,
+ case .newtab = firstTab.content,
+ !newTab {
+ firstTab.setContent(url.map { .url($0, source: source) } ?? .newtab)
+ } else if let tab = tabCollectionViewModel.selectedTabViewModel?.tab, !newTab {
+ tab.setContent(url.map { .url($0, source: source) } ?? .newtab)
+ } else {
+ let newTab = Tab(content: url.map { .url($0, source: source) } ?? .newtab, shouldLoadInBackground: true, burnerMode: tabCollectionViewModel.burnerMode)
+ newTab.setContent(url.map { .url($0, source: source) } ?? .newtab)
+ tabCollectionViewModel.append(tab: newTab)
+ }
+ }
+
func showTab(with content: Tab.TabContent) {
guard let windowController = self.mainWindowController else {
let tabCollection = TabCollection(tabs: [Tab(content: content)])
diff --git a/DuckDuckGo/Windows/View/WindowsManager.swift b/DuckDuckGo/Windows/View/WindowsManager.swift
index d19dad2a42..bfe93e0329 100644
--- a/DuckDuckGo/Windows/View/WindowsManager.swift
+++ b/DuckDuckGo/Windows/View/WindowsManager.swift
@@ -23,7 +23,11 @@ import BrowserServicesKit
final class WindowsManager {
class var windows: [NSWindow] {
- return NSApplication.shared.windows
+ NSApplication.shared.windows
+ }
+
+ class var mainWindows: [MainWindow] {
+ NSApplication.shared.windows.compactMap { $0 as? MainWindow }
}
class func closeWindows(except windows: [NSWindow] = []) {
diff --git a/DuckDuckGoVPN/NetworkProtectionBouncer.swift b/DuckDuckGoVPN/NetworkProtectionBouncer.swift
index 261121df37..2f3d8d2bdb 100644
--- a/DuckDuckGoVPN/NetworkProtectionBouncer.swift
+++ b/DuckDuckGoVPN/NetworkProtectionBouncer.swift
@@ -30,7 +30,7 @@ final class NetworkProtectionBouncer {
/// current app.
///
func requireAuthTokenOrKillApp() {
- let keychainStore = NetworkProtectionKeychainTokenStore(keychainType: .default, errorEvents: nil)
+ let keychainStore = NetworkProtectionKeychainTokenStore(keychainType: .default, errorEvents: nil, isSubscriptionEnabled: false)
guard keychainStore.isFeatureActivated else {
os_log(.error, log: .networkProtection, "π΄ Stopping: Network Protection not authorized.")
diff --git a/DuckDuckGoVPN/TunnelControllerIPCService.swift b/DuckDuckGoVPN/TunnelControllerIPCService.swift
index 2073438e40..c8a9ce456d 100644
--- a/DuckDuckGoVPN/TunnelControllerIPCService.swift
+++ b/DuckDuckGoVPN/TunnelControllerIPCService.swift
@@ -29,14 +29,14 @@ import NetworkProtectionUI
/// Clients can edit those defaults and this class will observe the changes and relay them to the runnel.
///
final class TunnelControllerIPCService {
- private let tunnelController: TunnelController
+ private let tunnelController: NetworkProtectionTunnelController
private let networkExtensionController: NetworkExtensionController
private let server: NetworkProtectionIPC.TunnelControllerIPCServer
private let statusReporter: NetworkProtectionStatusReporter
private var cancellables = Set()
private let defaults: UserDefaults
- init(tunnelController: TunnelController,
+ init(tunnelController: NetworkProtectionTunnelController,
networkExtensionController: NetworkExtensionController,
statusReporter: NetworkProtectionStatusReporter,
defaults: UserDefaults = .netP) {
@@ -112,6 +112,8 @@ extension TunnelControllerIPCService: IPCServerInterface {
}
func debugCommand(_ command: DebugCommand) async throws {
+ try await tunnelController.relay(command)
+
switch command {
case .removeSystemExtension:
#if NETP_SYSTEM_EXTENSION
diff --git a/IntegrationTests/History/HistoryIntegrationTests.swift b/IntegrationTests/History/HistoryIntegrationTests.swift
index 95eb98129d..65bedef872 100644
--- a/IntegrationTests/History/HistoryIntegrationTests.swift
+++ b/IntegrationTests/History/HistoryIntegrationTests.swift
@@ -20,6 +20,7 @@ import Combine
import Common
import Navigation
import XCTest
+import History
@testable import DuckDuckGo_Privacy_Browser
@available(macOS 12.0, *)
diff --git a/IntegrationTests/Tab/ErrorPageTests.swift b/IntegrationTests/Tab/ErrorPageTests.swift
index 8e6bf5c61b..bb89e9c535 100644
--- a/IntegrationTests/Tab/ErrorPageTests.swift
+++ b/IntegrationTests/Tab/ErrorPageTests.swift
@@ -143,23 +143,25 @@ class ErrorPageTests: XCTestCase {
NSError.disableSwizzledDescription = false
}
+ // MARK: - Tests
+
func testWhenPageFailsToLoad_errorPageShown() async throws {
// open Tab with newtab page
let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
+ let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab]))
window = WindowsManager.openNewWindow(with: viewModel)!
- let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
try await eNewtabPageLoaded.value
// navigate to test url, fail with error
schemeHandler.middleware = [{ _ in
.failure(NSError.hostNotFound)
}]
- tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString)))
let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise()
let eNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
+ tab.setContent(.url(.test, source: .userEntered(URL.test.absoluteString)))
let error = try await eNavigationFailed.value
_=try await eNavigationFinished.value
@@ -187,39 +189,43 @@ class ErrorPageTests: XCTestCase {
// open 2 Tabs with newtab page
let tab1 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
+ let eNewtabPageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
+ let eNewtab2PageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2]))
+ tabsViewModel.select(at: .unpinned(0))
window = WindowsManager.openNewWindow(with: tabsViewModel)!
// wait until Home page loads
- let eNewtabPageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
try await eNewtabPageLoaded.value
+ try await eNewtab2PageLoaded.value
// navigate to a failing url
+ let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise()
+ let eErrorPageLoaded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
schemeHandler.middleware = [{ _ in
.failure(NSError.noConnection)
}]
+
tab1.setContent(.url(.test, source: .userEntered(URL.test.absoluteString)))
// wait for error page to open
- let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise()
-
_=try await eNavigationFailed.value
+ _=try await eErrorPageLoaded.value
// switch to tab 2
tabsViewModel.select(at: .unpinned(1))
// next load should be ok
let eServerQueried = expectation(description: "server request sent")
+ let eNavigationSucceeded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
schemeHandler.middleware = [{ _ in
eServerQueried.fulfill()
return .ok(.html(Self.testHtml))
}]
// coming back to the failing tab 1 should trigger its reload
- let eNavigationSucceeded = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
-
tabsViewModel.select(at: .unpinned(0))
+ await fulfillment(of: [eServerQueried], timeout: 5)
_=try await eNavigationSucceeded.value
- await fulfillment(of: [eServerQueried], timeout: 1)
XCTAssertEqual(tab1.content.url, .test)
XCTAssertNil(tab1.error)
}
@@ -232,13 +238,13 @@ class ErrorPageTests: XCTestCase {
}]
let tab1 = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
+ let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise()
+ let eNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
+
let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2]))
window = WindowsManager.openNewWindow(with: tabsViewModel)!
// wait for error page to open
- let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise()
- let eNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
-
_=try await eNavigationFailed.value
_=try await eNavigationFinished.value
@@ -285,12 +291,12 @@ class ErrorPageTests: XCTestCase {
}]
let tab1 = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
let tab2 = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
+ let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise()
+ let errorNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
let tabsViewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab1, tab2]))
window = WindowsManager.openNewWindow(with: tabsViewModel)!
// wait for error page to open
- let eNavigationFailed = tab1.$error.compactMap { $0 }.timeout(5).first().promise()
- let errorNavigationFinished = tab1.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
_=try await eNavigationFailed.value
_=try await errorNavigationFinished.value
@@ -314,11 +320,11 @@ class ErrorPageTests: XCTestCase {
.failure(NSError.hostNotFound)
}]
let tab = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
+ let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise()
+ let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
window = WindowsManager.openNewWindow(with: tab)!
// wait for navigation to fail
- let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise()
- let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
_=try await eNavigationFailed.value
_=try await errorNavigationFinished.value
@@ -366,11 +372,11 @@ class ErrorPageTests: XCTestCase {
.failure(NSError.hostNotFound)
}]
let tab = Tab(content: .url(.test, source: .link), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
+ let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise()
+ let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
window = WindowsManager.openNewWindow(with: tab)!
// wait for navigation to fail
- let eNavigationFailed = tab.$error.compactMap { $0 }.timeout(5).first().promise()
- let errorNavigationFinished = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
_=try await eNavigationFailed.value
_=try await errorNavigationFinished.value
@@ -420,9 +426,9 @@ class ErrorPageTests: XCTestCase {
func testWhenPageLoadedAndFailsOnRefreshAndOnConsequentRefresh_errorPageIsUpdatedKeepingForwardHistory() async throws {
// open Tab with newtab page
let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
+ let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab]))
window = WindowsManager.openNewWindow(with: viewModel)!
- let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
try await eNewtabPageLoaded.value
// navigate to test url, success
@@ -496,9 +502,9 @@ class ErrorPageTests: XCTestCase {
func testWhenPageLoadedAndFailsOnRefreshAndSucceedsOnConsequentRefresh_forwardHistoryIsPreserved() async throws {
// open Tab with newtab page
let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
+ let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab]))
window = WindowsManager.openNewWindow(with: viewModel)!
- let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
try await eNewtabPageLoaded.value
// navigate to test url, success
@@ -564,9 +570,9 @@ class ErrorPageTests: XCTestCase {
func testWhenReloadingBySubmittingSameURL_errorPageRemainsSame() async throws {
// open Tab with newtab page
let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
+ let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab]))
window = WindowsManager.openNewWindow(with: viewModel)!
- let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
try await eNewtabPageLoaded.value
// navigate to test url, success
@@ -639,9 +645,9 @@ class ErrorPageTests: XCTestCase {
func testWhenGoingToAnotherUrlFails_newBackForwardHistoryItemIsAdded() async throws {
// open Tab with newtab page
let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
+ let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab]))
window = WindowsManager.openNewWindow(with: viewModel)!
- let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
try await eNewtabPageLoaded.value
// navigate to test url, success
@@ -714,9 +720,9 @@ class ErrorPageTests: XCTestCase {
func testWhenGoingToAnotherUrlSucceeds_newBackForwardHistoryItemIsAdded() async throws {
// open Tab with newtab page
let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
+ let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab]))
window = WindowsManager.openNewWindow(with: viewModel)!
- let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
try await eNewtabPageLoaded.value
// navigate to test url, success
@@ -786,9 +792,9 @@ class ErrorPageTests: XCTestCase {
}]
let tab = Tab(content: .url(.test, source: .pendingStateRestoration), webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock, interactionStateData: Self.sessionStateData)
+ let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab]))
window = WindowsManager.openNewWindow(with: viewModel)!
- let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
try await eNewtabPageLoaded.value
XCTAssertTrue(tab.canReload)
@@ -871,8 +877,8 @@ class ErrorPageTests: XCTestCase {
// open Tab with newtab page
let tab = Tab(content: .newtab, webViewConfiguration: webViewConfiguration, privacyFeatures: privacyFeaturesMock)
let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab]))
- window = WindowsManager.openNewWindow(with: viewModel)!
let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
+ window = WindowsManager.openNewWindow(with: viewModel)!
try await eNewtabPageLoaded.value
// navigate to alt url, redirect to test url, fail with error
diff --git a/IntegrationTests/Tab/SearchNonexistentDomainTests.swift b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift
index 346231ede7..b715d829e5 100644
--- a/IntegrationTests/Tab/SearchNonexistentDomainTests.swift
+++ b/IntegrationTests/Tab/SearchNonexistentDomainTests.swift
@@ -21,6 +21,7 @@ import Carbon
import Combine
import Navigation
import XCTest
+import History
@testable import DuckDuckGo_Privacy_Browser
diff --git a/IntegrationTests/Tab/TabContentTests.swift b/IntegrationTests/Tab/TabContentTests.swift
new file mode 100644
index 0000000000..13ba1fc393
--- /dev/null
+++ b/IntegrationTests/Tab/TabContentTests.swift
@@ -0,0 +1,349 @@
+//
+// TabContentTests.swift
+//
+// Copyright Β© 2024 DuckDuckGo. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+import AppKit
+import Carbon
+import Combine
+import Common
+import XCTest
+@testable import DuckDuckGo_Privacy_Browser
+
+@available(macOS 12.0, *)
+@MainActor
+class TabContentTests: XCTestCase {
+
+ var window: NSWindow!
+
+ var mainViewController: MainViewController {
+ (window.contentViewController as! MainViewController)
+ }
+
+ var tabViewModel: TabViewModel {
+ mainViewController.browserTabViewController.tabViewModel!
+ }
+
+ @MainActor
+ override func setUp() async throws {
+ }
+
+ @MainActor
+ override func tearDown() async throws {
+ window?.close()
+ window = nil
+ NSView.swizzleWillOpenMenu(with: nil)
+ }
+
+ func sendRightMouseClick(to view: NSView) {
+ let point = view.convert(view.bounds.center, to: nil)
+
+ let mouseDown = NSEvent.mouseEvent(with: .rightMouseDown,
+ location: point,
+ modifierFlags: [],
+ timestamp: CACurrentMediaTime(),
+ windowNumber: window.windowNumber,
+ context: nil,
+ eventNumber: -22966,
+ clickCount: 1,
+ pressure: 1)!
+ let mouseUp = NSEvent.mouseEvent(with: .rightMouseUp,
+ location: point,
+ modifierFlags: [],
+ timestamp: CACurrentMediaTime(),
+ windowNumber: window.windowNumber,
+ context: nil,
+ eventNumber: -22966,
+ clickCount: 1,
+ pressure: 1)!
+ view.window!.sendEvent(mouseDown)
+ view.window!.sendEvent(mouseUp)
+ }
+
+ // MARK: - Tests
+
+ @MainActor
+ func testWhenPDFContextMenuPrintChosen_printDialogOpens() async throws {
+ let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")!
+ // open Tab with PDF
+ let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered("")))
+ let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab]))
+ window = WindowsManager.openNewWindow(with: viewModel)!
+ let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
+ try await eNewtabPageLoaded.value
+
+ // wait for context menu to appear
+ let eMenuShown = expectation(description: "menu shown")
+ var menuItems = [NSMenuItem]()
+ NSView.swizzleWillOpenMenu { menu, event in
+ menuItems = menu.items
+ menu.removeAllItems()
+ eMenuShown.fulfill()
+ }
+
+ // right-click
+ NSApp.activate(ignoringOtherApps: true)
+ sendRightMouseClick(to: tab.webView)
+ await fulfillment(of: [eMenuShown])
+
+ // find Print, Save As
+ guard let printMenuItem = menuItems.first(where: { $0.title == UserText.printMenuItem }) else {
+ XCTFail("No print menu item")
+ return
+ }
+ let saveAsMenuItem = menuItems.first(where: { $0.title == UserText.mainMenuFileSaveAs })
+ XCTAssertNotNil(saveAsMenuItem)
+
+ // wait for print dialog to appear
+ let ePrintDialogShown = expectation(description: "Print dialog shown")
+ let getPrintDialog = Task { @MainActor in
+ while true {
+ if let sheet = self.window.sheets.first {
+ ePrintDialogShown.fulfill()
+ return sheet
+ }
+ try await Task.sleep(interval: 0.01)
+ }
+ }
+ let printOperationPromise = tab.$userInteractionDialog.compactMap { (dialog: Tab.UserDialog?) -> NSPrintOperation? in
+ guard case .print(let request) = dialog?.dialog else { return nil }
+ return request.parameters
+ }.timeout(5).first().promise()
+
+ XCTAssertNotNil(printMenuItem.action)
+ XCTAssertNotNil(printMenuItem.pdfHudRepresentedObject)
+
+ // Click Printβ¦
+ _=printMenuItem.action.map { action in
+ NSApp.sendAction(action, to: printMenuItem.target, from: printMenuItem)
+ }
+ if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [ePrintDialogShown], timeout: 5) {
+ getPrintDialog.cancel()
+ }
+ let printDialog = try await getPrintDialog.value
+ defer {
+ window.endSheet(printDialog, returnCode: .cancel)
+ }
+ let printOperation = try await printOperationPromise.value
+
+ XCTAssertEqual(printDialog.title, UserText.printMenuItem.dropping(suffix: "β¦"))
+ XCTAssertEqual(printOperation.pageRange, NSRange(location: 1, length: 3))
+ }
+
+ @MainActor
+ func testWhenPDFMainMenuPrintChosen_printDialogOpens() async throws {
+ let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")!
+ // open Tab with PDF
+ let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered("")))
+ let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab]))
+ window = WindowsManager.openNewWindow(with: viewModel)!
+ let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
+ try await eNewtabPageLoaded.value
+
+ // wait for print dialog to appear
+ let ePrintDialogShown = expectation(description: "Print dialog shown")
+ let getPrintDialog = Task { @MainActor in
+ while true {
+ if let sheet = self.window.sheets.first {
+ ePrintDialogShown.fulfill()
+ return sheet
+ }
+ try await Task.sleep(interval: 0.01)
+ }
+ }
+ let printOperationPromise = tab.$userInteractionDialog.compactMap { (dialog: Tab.UserDialog?) -> NSPrintOperation? in
+ guard case .print(let request) = dialog?.dialog else { return nil }
+ return request.parameters
+ }.timeout(5).first().promise()
+
+ // Hit Cmd+P
+ let keyDown = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [.command], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "p", charactersIgnoringModifiers: "p", isARepeat: false, keyCode: UInt16(kVK_ANSI_P))!
+ let keyUp = NSEvent.keyEvent(with: .keyUp, location: .zero, modifierFlags: [.command], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "p", charactersIgnoringModifiers: "p", isARepeat: false, keyCode: UInt16(kVK_ANSI_P))!
+ window.sendEvent(keyDown)
+ window.sendEvent(keyUp)
+
+ if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [ePrintDialogShown], timeout: 5) {
+ getPrintDialog.cancel()
+ }
+ let printDialog = try await getPrintDialog.value
+ defer {
+ window.endSheet(printDialog, returnCode: .cancel)
+ }
+ let printOperation = try await printOperationPromise.value
+
+ XCTAssertEqual(printDialog.title, UserText.printMenuItem.dropping(suffix: "β¦"))
+ XCTAssertEqual(printOperation.pageRange, NSRange(location: 1, length: 3))
+ }
+
+ @MainActor
+ func testWhenPDFContextMenuSaveAsChosen_saveDialogOpens() async throws {
+ let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")!
+ // open Tab with PDF
+ let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered("")))
+ let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab]))
+ window = WindowsManager.openNewWindow(with: viewModel)!
+ let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
+ try await eNewtabPageLoaded.value
+
+ // wait for context menu to appear
+ let eMenuShown = expectation(description: "menu shown")
+ var menuItems = [NSMenuItem]()
+ NSView.swizzleWillOpenMenu { menu, event in
+ menuItems = menu.items
+ menu.removeAllItems()
+ eMenuShown.fulfill()
+ }
+
+ // right-click
+ NSApp.activate(ignoringOtherApps: true)
+ sendRightMouseClick(to: tab.webView)
+ await fulfillment(of: [eMenuShown])
+
+ // find Print, Save As
+ let printMenuItem = menuItems.first(where: { $0.title == UserText.printMenuItem })
+ XCTAssertNotNil(printMenuItem)
+ guard let saveAsMenuItem = menuItems.first(where: { $0.title == UserText.mainMenuFileSaveAs }) else {
+ XCTFail("No Save As menu item")
+ return
+ }
+
+ // wait for print dialog to appear
+ let eSaveDialogShown = expectation(description: "Save dialog shown")
+ let getSaveDialog = Task { @MainActor in
+ while true {
+ if let sheet = self.window.sheets.first as? NSSavePanel {
+ eSaveDialogShown.fulfill()
+ return sheet
+ }
+ try await Task.sleep(interval: 0.01)
+ }
+ }
+
+ XCTAssertNotNil(saveAsMenuItem.action)
+ XCTAssertNotNil(saveAsMenuItem.pdfHudRepresentedObject)
+
+ // Click Save Asβ¦
+ _=saveAsMenuItem.action.map { action in
+ NSApp.sendAction(action, to: saveAsMenuItem.target, from: saveAsMenuItem)
+ }
+ if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [eSaveDialogShown], timeout: 5) {
+ getSaveDialog.cancel()
+ }
+ let saveDialog = try await getSaveDialog.value
+
+ guard let url = saveDialog.url else {
+ XCTFail("no Save Dialog url")
+ return
+ }
+ try? FileManager.default.removeItem(at: url)
+
+ // wait until file is saved
+ let fileSavedPromise = Timer.publish(every: 0.01, on: .main, in: .default).autoconnect().filter { _ in
+ FileManager.default.fileExists(atPath: url.path)
+ }.timeout(5).first().promise()
+ defer {
+ try? FileManager.default.removeItem(at: url)
+ }
+
+ window.endSheet(saveDialog, returnCode: .OK)
+
+ _=try await fileSavedPromise.value
+ try XCTAssertEqual(Data(contentsOf: url), Data(contentsOf: pdfUrl))
+ }
+
+ @MainActor
+ func testWhenPDFMainMenuSaveAsChosen_saveDialogOpens() async throws {
+ let pdfUrl = Bundle(for: Self.self).url(forResource: "test", withExtension: "pdf")!
+ // open Tab with PDF
+ let tab = Tab(content: .url(pdfUrl, credential: nil, source: .userEntered("")))
+ let viewModel = TabCollectionViewModel(tabCollection: TabCollection(tabs: [tab]))
+ window = WindowsManager.openNewWindow(with: viewModel)!
+ let eNewtabPageLoaded = tab.webViewDidFinishNavigationPublisher.timeout(5).first().promise()
+ try await eNewtabPageLoaded.value
+
+ // wait for print dialog to appear
+ let eSaveDialogShown = expectation(description: "Save dialog shown")
+ let getSaveDialog = Task { @MainActor in
+ while true {
+ if let sheet = self.window.sheets.first as? NSSavePanel {
+ eSaveDialogShown.fulfill()
+ return sheet
+ }
+ try await Task.sleep(interval: 0.01)
+ }
+ }
+
+ // Hit Cmd+S
+ let keyDown = NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [.command], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "s", charactersIgnoringModifiers: "s", isARepeat: false, keyCode: UInt16(kVK_ANSI_S))!
+ let keyUp = NSEvent.keyEvent(with: .keyUp, location: .zero, modifierFlags: [.command], timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: "s", charactersIgnoringModifiers: "s", isARepeat: false, keyCode: UInt16(kVK_ANSI_S))!
+ window.sendEvent(keyDown)
+ window.sendEvent(keyUp)
+
+ if case .timedOut = await XCTWaiter(delegate: self).fulfillment(of: [eSaveDialogShown], timeout: 5) {
+ getSaveDialog.cancel()
+ }
+ let saveDialog = try await getSaveDialog.value
+
+ guard let url = saveDialog.url else {
+ XCTFail("no Save Dialog url")
+ return
+ }
+ try? FileManager.default.removeItem(at: url)
+
+ // wait until file is saved
+ let fileSavedPromise = Timer.publish(every: 0.01, on: .main, in: .default).autoconnect().filter { _ in
+ FileManager.default.fileExists(atPath: url.path)
+ }.timeout(5).first().promise()
+ defer {
+ try? FileManager.default.removeItem(at: url)
+ }
+
+ window.endSheet(saveDialog, returnCode: .OK)
+
+ _=try await fileSavedPromise.value
+ try XCTAssertEqual(Data(contentsOf: url), Data(contentsOf: pdfUrl))
+ }
+
+}
+
+private extension NSView {
+
+ private static var willOpenMenuWithEvent: ((NSMenu, NSEvent) -> Void)?
+
+ private static let originalWillOpenMenu = {
+ class_getInstanceMethod(NSView.self, #selector(NSView.willOpenMenu))!
+ }()
+ private static let swizzledWillOpenMenu = {
+ class_getInstanceMethod(NSView.self, #selector(NSView.swizzled_willOpenMenu))!
+ }()
+ private static let swizzleWillOpenMenuOnce: Void = {
+ method_exchangeImplementations(originalWillOpenMenu, swizzledWillOpenMenu)
+ }()
+
+ static func swizzleWillOpenMenu(with willOpenMenuWithEvent: ((NSMenu, NSEvent) -> Void)?) {
+ _=swizzleWillOpenMenuOnce
+ self.willOpenMenuWithEvent = willOpenMenuWithEvent
+ }
+
+ @objc dynamic func swizzled_willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
+ if let willOpenMenuWithEvent = Self.willOpenMenuWithEvent {
+ willOpenMenuWithEvent(menu, event)
+ } else {
+ self.swizzled_willOpenMenu(menu, with: event) // call original
+ }
+ }
+
+}
diff --git a/IntegrationTests/Tab/test.pdf b/IntegrationTests/Tab/test.pdf
new file mode 100644
index 0000000000..fd772c0fc0
Binary files /dev/null and b/IntegrationTests/Tab/test.pdf differ
diff --git a/LocalPackages/DataBrokerProtection/.swiftpm/xcode/xcshareddata/xcschemes/DataBrokerProtectionTests.xcscheme b/LocalPackages/DataBrokerProtection/.swiftpm/xcode/xcshareddata/xcschemes/DataBrokerProtectionTests.xcscheme
index 31e1a52da2..5ba440572a 100644
--- a/LocalPackages/DataBrokerProtection/.swiftpm/xcode/xcshareddata/xcschemes/DataBrokerProtectionTests.xcscheme
+++ b/LocalPackages/DataBrokerProtection/.swiftpm/xcode/xcshareddata/xcschemes/DataBrokerProtectionTests.xcscheme
@@ -1,6 +1,6 @@
Void) {
+ // todo
+ }
}
diff --git a/Submodules/privacy-reference-tests b/Submodules/privacy-reference-tests
index 40ce86837d..a603ff9af2 160000
--- a/Submodules/privacy-reference-tests
+++ b/Submodules/privacy-reference-tests
@@ -1 +1 @@
-Subproject commit 40ce86837def0adbf558f00ed0531ab4df5839a8
+Subproject commit a603ff9af22ca3ff7ce2e7ffbfe18c447d9f23e8
diff --git a/UnitTests/Common/CoreDataTestUtilities.swift b/UnitTests/Common/CoreDataTestUtilities.swift
index e9963e2cc8..60366e73ab 100644
--- a/UnitTests/Common/CoreDataTestUtilities.swift
+++ b/UnitTests/Common/CoreDataTestUtilities.swift
@@ -55,31 +55,7 @@ final class CoreData {
return createInMemoryPersistentContainer(modelName: "CoreDataEncryptionTesting", bundle: Bundle(for: CoreData.self))
}
- static func registerValueTransformer(for propertyClass: AnyClass, with keyStore: EncryptionKeyStoring) -> NSValueTransformerName {
- guard let encodableType = propertyClass as? (NSObject & NSSecureCoding).Type else {
- fatalError("Unsupported type")
- }
- func registerValueTransformer(for type: T.Type) -> NSValueTransformerName {
- (try? EncryptedValueTransformer.registerTransformer(keyStore: keyStore))!
- return EncryptedValueTransformer.transformerName
- }
- return registerValueTransformer(for: encodableType)
- }
-
- static func registerValueTransformers(for managedObjectModel: NSManagedObjectModel) -> [NSValueTransformerName] {
- let storedClasses = managedObjectModel.entities.reduce(into: Set()) { result, entity in
- result.formUnion(entity.properties.compactMap { property in
- (property as? NSAttributeDescription)?.attributeValueClassName
- })
- }.compactMap(NSClassFromString)
-
- let keyStore = EncryptionKeyStoreMock()
- return storedClasses.map { propertyClass in
- registerValueTransformer(for: propertyClass, with: keyStore)
- }
- }
-
- static func createPersistentContainer(at url: URL, modelName: String, bundle: Bundle) -> NSPersistentContainer {
+ static func createPersistentContainer(at url: URL, modelName: String, bundle: Bundle, keyStore: EncryptionKeyStoring) -> NSPersistentContainer {
guard let modelURL = bundle.url(forResource: modelName, withExtension: "momd") else {
fatalError("Error loading model from bundle")
}
@@ -88,7 +64,7 @@ final class CoreData {
fatalError("Error initializing object model from: \(modelURL)")
}
- let transformers = registerValueTransformers(for: objectModel)
+ let transformers = objectModel.registerValueTransformers(keyStore: keyStore)
let container = TestPersistentContainer(name: modelName,
managedObjectModel: objectModel,
registeredTransformers: transformers)
@@ -106,13 +82,13 @@ final class CoreData {
return container
}
- static func createInMemoryPersistentContainer(modelName: String, bundle: Bundle) -> NSPersistentContainer {
+ static func createInMemoryPersistentContainer(modelName: String, bundle: Bundle, keyStore: EncryptionKeyStoring = EncryptionKeyStoreMock()) -> NSPersistentContainer {
// Creates a persistent store using the in-memory model, no state will be written to disk.
// This was the approach I had seen recommended in a WWDC session, but there is also a
// `NSInMemoryStoreType` option for doing this.
//
// This approach is apparently the recommended choice: https://www.donnywals.com/setting-up-a-core-data-store-for-unit-tests/
- return createPersistentContainer(at: URL(fileURLWithPath: "/dev/null"), modelName: modelName, bundle: bundle)
+ return createPersistentContainer(at: URL(fileURLWithPath: "/dev/null"), modelName: modelName, bundle: bundle, keyStore: keyStore)
}
}
diff --git a/UnitTests/Common/FileSystem/CoreDataEncryptionTests.swift b/UnitTests/Common/FileSystem/CoreDataEncryptionTests.swift
index c4774385d7..0602ec4116 100644
--- a/UnitTests/Common/FileSystem/CoreDataEncryptionTests.swift
+++ b/UnitTests/Common/FileSystem/CoreDataEncryptionTests.swift
@@ -32,6 +32,10 @@ final class CoreDataEncryptionTests: XCTestCase {
return transformer
}()
+ static var container = CoreData.encryptionContainer()
+ static var context = container.viewContext
+ var context: NSManagedObjectContext { Self.context }
+
override func setUp() {
super.setUp()
@@ -39,9 +43,6 @@ final class CoreDataEncryptionTests: XCTestCase {
}
func testSavingEncryptedValues() {
- let container = CoreData.encryptionContainer()
- let context = container.viewContext
-
context.performAndWait {
let entity = PartiallyEncryptedEntity(context: context)
entity.date = Date()
@@ -56,8 +57,6 @@ final class CoreDataEncryptionTests: XCTestCase {
}
func testFetchingEncryptedValues() {
- let container = CoreData.encryptionContainer()
- let context = container.viewContext
let timestamp = Date()
context.performAndWait {
@@ -81,8 +80,6 @@ final class CoreDataEncryptionTests: XCTestCase {
func testValueTransformers() {
let transformer = self.mockValueTransformer
- let container = CoreData.encryptionContainer()
- let context = container.viewContext
context.performAndWait {
let entity = MockEntity(context: context)
diff --git a/UnitTests/FileDownload/DownloadListCoordinatorTests.swift b/UnitTests/FileDownload/DownloadListCoordinatorTests.swift
index 4cb67403f7..b131448893 100644
--- a/UnitTests/FileDownload/DownloadListCoordinatorTests.swift
+++ b/UnitTests/FileDownload/DownloadListCoordinatorTests.swift
@@ -63,7 +63,7 @@ final class DownloadListCoordinatorTests: XCTestCase {
func setUpCoordinatorAndAddDownload(isBurner: Bool = false) -> (WKDownloadMock, WebKitDownloadTask, UUID) {
setUpCoordinator()
- let download = WKDownloadMock()
+ let download = WKDownloadMock(url: .duckDuckGo)
let task = WebKitDownloadTask(download: download, promptForLocation: false, destinationURL: destURL, tempURL: tempURL, isBurner: isBurner)
let e = expectation(description: "download added")
@@ -144,7 +144,7 @@ final class DownloadListCoordinatorTests: XCTestCase {
func testWhenDownloadAddedThenDownloadItemIsPublished() {
setUpCoordinator()
- let task = WebKitDownloadTask(download: WKDownloadMock(), promptForLocation: false, destinationURL: destURL, tempURL: tempURL, isBurner: false)
+ let task = WebKitDownloadTask(download: WKDownloadMock(url: .duckDuckGo), promptForLocation: false, destinationURL: destURL, tempURL: tempURL, isBurner: false)
let e = expectation(description: "download added")
let c = coordinator.updates.sink { [coordinator] (kind, item) in
@@ -245,7 +245,7 @@ final class DownloadListCoordinatorTests: XCTestCase {
webView.resumeDownloadBlock = { data in
resumeCalled.fulfill()
XCTAssertEqual(data, .resumeData)
- return WKDownloadMock()
+ return WKDownloadMock(url: .duckDuckGo)
}
webView.startDownloadBlock = { _ in
XCTFail("unexpected start call")
@@ -298,7 +298,7 @@ final class DownloadListCoordinatorTests: XCTestCase {
webView.resumeDownloadBlock = { data in
resumeCalled.fulfill()
XCTAssertEqual(data, .resumeData)
- return WKDownloadMock()
+ return WKDownloadMock(url: .duckDuckGo)
}
webView.startDownloadBlock = { _ in
XCTFail("unexpected start call")
@@ -354,7 +354,7 @@ final class DownloadListCoordinatorTests: XCTestCase {
webView.startDownloadBlock = { request in
startCalled.fulfill()
XCTAssertEqual(request?.url, item.url)
- return WKDownloadMock()
+ return WKDownloadMock(url: .duckDuckGo)
}
let downloadAdded = expectation(description: "download addeed")
diff --git a/UnitTests/FileDownload/DownloadsTabExtensionTests.swift b/UnitTests/FileDownload/DownloadsTabExtensionTests.swift
index 55b835e12f..c3747aea31 100644
--- a/UnitTests/FileDownload/DownloadsTabExtensionTests.swift
+++ b/UnitTests/FileDownload/DownloadsTabExtensionTests.swift
@@ -22,8 +22,10 @@ import UniformTypeIdentifiers
@testable import DuckDuckGo_Privacy_Browser
+@MainActor
final class DownloadsTabExtensionTests: XCTestCase {
private var testData: Data!
+ private var testOriginatingURL: URL!
private let filename = "Document.pdf"
private let fileManager = FileManager.default
private var testDirectory: URL!
@@ -34,6 +36,9 @@ final class DownloadsTabExtensionTests: XCTestCase {
testData = try XCTUnwrap("test".data(using: .utf8))
testDirectory = fileManager.temporaryDirectory
+ testOriginatingURL = testDirectory.appendingPathComponent(UUID().uuidString + ".pdf")
+ try testData.write(to: testOriginatingURL)
+
cancellables = []
}
@@ -44,27 +49,61 @@ final class DownloadsTabExtensionTests: XCTestCase {
try super.tearDownWithError()
}
- @MainActor
- func testWhenAlwaysRequestDownloadLocationIsTrueThenShouldAskDownloadsTabExtensionToSaveData() throws {
+ func testWhenAlwaysRequestDownloadLocationIsTrueThenShouldAskDownloadsTabExtensionToSaveData() async throws {
// GIVEN
let expectedURL = testDirectory.appendingPathComponent(filename)
let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: true)
XCTAssertFalse(fileManager.fileExists(atPath: expectedURL.path))
// WHEN
- sut.saveDownloaded(data: testData, suggestedFilename: filename, mimeType: "application/pdf")
sut.$savePanelDialogRequest
.sink { request in
request?.submit( (url: expectedURL, fileType: nil))
}
.store(in: &cancellables)
+ _=try await sut.saveDownloadedData(testData, suggestedFilename: filename, mimeType: "application/pdf",
+ originatingURL: testOriginatingURL.appendingPathExtension("fake"))
// THEN
XCTAssertTrue(fileManager.fileExists(atPath: expectedURL.path))
}
- @MainActor
- func testWhenSaveDataAndFileExistThenURLShouldIncrementIndex() throws {
+ func testWhenAlwaysRequestDownloadLocationIsTrueAndNoDataProvidedAndOriginatingFileURLProvidedThenShouldAskDownloadsTabExtensionToSaveData() async throws {
+ // GIVEN
+ let expectedURL = testDirectory.appendingPathComponent(filename)
+ let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: true)
+ XCTAssertFalse(fileManager.fileExists(atPath: expectedURL.path))
+
+ // WHEN
+ sut.$savePanelDialogRequest
+ .sink { request in
+ request?.submit( (url: expectedURL, fileType: nil))
+ }
+ .store(in: &cancellables)
+ _=try await sut.saveDownloadedData(nil, suggestedFilename: filename, mimeType: "application/pdf", originatingURL: testOriginatingURL)
+
+ // THEN
+ XCTAssertTrue(fileManager.fileExists(atPath: expectedURL.path))
+ }
+
+ func testWhenSaveDataAndFileExistThenURLShouldIncrementIndex() async throws {
+ // GIVEN
+ let destURL = testDirectory.appendingPathComponent(filename)
+ let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: false)
+ let expectedFilename = "Document 1.pdf"
+ let expectedDestURL = testDirectory.appendingPathComponent(expectedFilename)
+ try testData.write(to: destURL)
+ XCTAssertFalse(fileManager.fileExists(atPath: expectedDestURL.path))
+
+ // WHEN
+ _=try await sut.saveDownloadedData(testData, suggestedFilename: filename, mimeType: "application/pdf",
+ originatingURL: testOriginatingURL.appendingPathExtension("fake"))
+
+ // THEN
+ XCTAssertTrue(fileManager.fileExists(atPath: expectedDestURL.path))
+ }
+
+ func testWhenNoDataProvidedAndOriginatingFileURLProvidedAndFileExistsThenFileShouldBeCopiedIncrementingIndex() async throws {
// GIVEN
let destURL = testDirectory.appendingPathComponent(filename)
let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: false)
@@ -74,21 +113,34 @@ final class DownloadsTabExtensionTests: XCTestCase {
XCTAssertFalse(fileManager.fileExists(atPath: expectedDestURL.path))
// WHEN
- sut.saveDownloaded(data: testData, suggestedFilename: filename, mimeType: "application/pdf")
+ _=try await sut.saveDownloadedData(nil, suggestedFilename: filename, mimeType: "application/pdf", originatingURL: testOriginatingURL)
// THEN
XCTAssertTrue(fileManager.fileExists(atPath: expectedDestURL.path))
}
- @MainActor
- func testWhenSaveDataAndFileDoesNotExistThenURLShouldNotIncrementIndex() throws {
+ func testWhenSaveDataAndFileDoesNotExistThenURLShouldNotIncrementIndex() async throws {
+ // GIVEN
+ let destURL = testDirectory.appendingPathComponent(filename)
+ let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: false)
+ XCTAssertFalse(fileManager.fileExists(atPath: destURL.path))
+
+ // WHEN
+ _=try await sut.saveDownloadedData(testData, suggestedFilename: filename, mimeType: "application/pdf",
+ originatingURL: testOriginatingURL.appendingPathExtension("fake"))
+
+ // THEN
+ XCTAssertTrue(fileManager.fileExists(atPath: destURL.path))
+ }
+
+ func testNoDataProvidedAndOriginatingFileURLProvidedAndFileDoesNotExistThenFileShouldBeCopiedNotIncrementingIndex() async throws {
// GIVEN
let destURL = testDirectory.appendingPathComponent(filename)
let sut = makeSUT(downloadLocation: testDirectory, alwaysRequestDownloadLocation: false)
XCTAssertFalse(fileManager.fileExists(atPath: destURL.path))
// WHEN
- sut.saveDownloaded(data: testData, suggestedFilename: filename, mimeType: "application/pdf")
+ _=try await sut.saveDownloadedData(nil, suggestedFilename: filename, mimeType: "application/pdf", originatingURL: testOriginatingURL)
// THEN
XCTAssertTrue(fileManager.fileExists(atPath: destURL.path))
diff --git a/UnitTests/FileDownload/FileDownloadManagerTests.swift b/UnitTests/FileDownload/FileDownloadManagerTests.swift
index 547646db35..02cc8b0c95 100644
--- a/UnitTests/FileDownload/FileDownloadManagerTests.swift
+++ b/UnitTests/FileDownload/FileDownloadManagerTests.swift
@@ -56,6 +56,7 @@ final class FileDownloadManagerTests: XCTestCase {
FileManager.restoreUrlsForIn()
self.chooseDestination = nil
self.fileIconFlyAnimationOriginalRect = nil
+ preferences.alwaysRequestDownloadLocation = false
}
func testWhenDownloadIsAddedThenItsPublished() {
@@ -64,7 +65,7 @@ final class FileDownloadManagerTests: XCTestCase {
e.fulfill()
}
- let download = WKDownloadMock()
+ let download = WKDownloadMock(url: .duckDuckGo)
dm.add(download, fromBurnerWindow: false, delegate: nil, location: .auto)
withExtendedLifetime(cancellable) {
@@ -78,7 +79,7 @@ final class FileDownloadManagerTests: XCTestCase {
e.fulfill()
}
- let download = WKDownloadMock()
+ let download = WKDownloadMock(url: .duckDuckGo)
dm.add(download, fromBurnerWindow: false, delegate: nil, location: .auto)
struct TestError: Error {}
@@ -96,7 +97,7 @@ final class FileDownloadManagerTests: XCTestCase {
e.fulfill()
}
- let download = WKDownloadMock()
+ let download = WKDownloadMock(url: .duckDuckGo)
dm.add(download, fromBurnerWindow: false, delegate: nil, location: .auto)
download.delegate?.downloadDidFinish!(download.asWKDownload())
@@ -122,7 +123,7 @@ final class FileDownloadManagerTests: XCTestCase {
callback(nil, nil)
}
- let download = WKDownloadMock()
+ let download = WKDownloadMock(url: .duckDuckGo)
dm.add(download, fromBurnerWindow: false, delegate: self, location: .prompt)
let url = URL(string: "https://duckduckgo.com/somefile.html")!
@@ -155,7 +156,7 @@ final class FileDownloadManagerTests: XCTestCase {
callback(localURL, .html)
}
- let download = WKDownloadMock()
+ let download = WKDownloadMock(url: .duckDuckGo)
dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto)
let url = URL(string: "https://duckduckgo.com/somefile.html")!
@@ -177,7 +178,7 @@ final class FileDownloadManagerTests: XCTestCase {
fm.createFile(atPath: localURL.path, contents: nil, attributes: nil)
XCTAssertTrue(fm.fileExists(atPath: localURL.path))
- let download = WKDownloadMock()
+ let download = WKDownloadMock(url: .duckDuckGo)
dm.add(download, fromBurnerWindow: false, delegate: self, location: .prompt)
self.chooseDestination = { _, _, _, callback in
callback(localURL, nil)
@@ -194,11 +195,38 @@ final class FileDownloadManagerTests: XCTestCase {
XCTAssertFalse(fm.fileExists(atPath: localURL.path))
}
+ func testWhenDownloadingLocalFileThenLocationChooserIsCalled() {
+ let downloadsURL = fm.temporaryDirectory
+ preferences.selectedDownloadLocation = downloadsURL
+
+ let download = WKDownloadMock(url: URL(fileURLWithPath: "/some/path"))
+
+ let localURL = downloadsURL.appendingPathComponent(testFile)
+ let e1 = expectation(description: "chooseDestinationCallback called")
+ self.chooseDestination = { suggestedFilename, directoryURL, fileTypes, callback in
+ dispatchPrecondition(condition: .onQueue(.main))
+ XCTAssertEqual(directoryURL, downloadsURL)
+ e1.fulfill()
+
+ callback(localURL, .html)
+ }
+
+ dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto)
+
+ let e2 = expectation(description: "WKDownload called")
+ download.delegate?.download(download.asWKDownload(), decideDestinationUsing: response, suggestedFilename: testFile) { [testFile] url in
+ XCTAssertEqual(url, downloadsURL.appendingPathComponent(testFile).appendingPathExtension(WebKitDownloadTask.downloadExtension))
+ e2.fulfill()
+ }
+
+ waitForExpectations(timeout: 0.3)
+ }
+
func testWhenNotRequiredByPreferencesThenDefaultDownloadLocationIsChosen() {
let downloadsURL = fm.temporaryDirectory
preferences.selectedDownloadLocation = downloadsURL
- let download = WKDownloadMock()
+ let download = WKDownloadMock(url: .duckDuckGo)
dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto)
self.chooseDestination = { _, _, _, _ in
XCTFail("Unpected chooseDestination call")
@@ -217,7 +245,7 @@ final class FileDownloadManagerTests: XCTestCase {
let downloadsURL = fm.temporaryDirectory
preferences.selectedDownloadLocation = downloadsURL
- let download = WKDownloadMock()
+ let download = WKDownloadMock(url: .duckDuckGo)
dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto)
self.chooseDestination = { _, _, _, _ in
XCTFail("Unpected chooseDestination call")
@@ -240,7 +268,7 @@ final class FileDownloadManagerTests: XCTestCase {
[URL(fileURLWithPath: "/")]
}
- let download = WKDownloadMock()
+ let download = WKDownloadMock(url: .duckDuckGo)
dm.add(download, fromBurnerWindow: false, delegate: self, location: .auto)
let e = expectation(description: "WKDownload called")
diff --git a/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift b/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift
index ed2ce03187..f2e14bf8d2 100644
--- a/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift
+++ b/UnitTests/FileDownload/Helpers/DownloadsTabExtensionMock.swift
@@ -27,10 +27,12 @@ class DownloadsTabExtensionMock: NSObject, DownloadsTabExtensionProtocol {
private(set) var didCallSaveWebViewContent = false
private(set) var capturedWebView: WKWebView?
+ @Published
private(set) var didCallSaveDownloadedData = false
private(set) var capturedSavedDownloadData: Data?
private(set) var capturedSuggestedFilename: String?
private(set) var capturedMimeType: String?
+ private(set) var capturedOriginatingURL: URL?
var savePanelDialogSubject = PassthroughSubject()
@@ -40,16 +42,19 @@ class DownloadsTabExtensionMock: NSObject, DownloadsTabExtensionProtocol {
savePanelDialogSubject.eraseToAnyPublisher()
}
- func saveWebViewContentAs(_ webView: WKWebView) {
+ func saveWebViewContent(from webView: WKWebView, pdfHUD: WKPDFHUDViewWrapper?, location: DownloadsTabExtension.DownloadLocation) {
didCallSaveWebViewContent = true
capturedWebView = webView
}
- func saveDownloaded(data: Data, suggestedFilename: String, mimeType: String) {
+ func saveDownloadedData(_ data: Data?, suggestedFilename: String, mimeType: String, originatingURL: URL) async throws -> URL? {
didCallSaveDownloadedData = true
capturedSavedDownloadData = data
capturedSuggestedFilename = suggestedFilename
capturedMimeType = mimeType
+ capturedOriginatingURL = originatingURL
+
+ return nil
}
func chooseDestination(suggestedFilename: String?, directoryURL: URL?, fileTypes: [UTType], callback: @escaping @MainActor (URL?, UTType?) -> Void) {}
diff --git a/UnitTests/FileDownload/Helpers/WKDownloadMock.swift b/UnitTests/FileDownload/Helpers/WKDownloadMock.swift
index a00a8a1bf4..acff9884a1 100644
--- a/UnitTests/FileDownload/Helpers/WKDownloadMock.swift
+++ b/UnitTests/FileDownload/Helpers/WKDownloadMock.swift
@@ -26,6 +26,10 @@ final class WKDownloadMock: NSObject, WebKitDownload, ProgressReporting {
var progress = Progress()
weak var delegate: WKDownloadDelegate?
+ init(url: URL) {
+ self.originalRequest = URLRequest(url: url)
+ }
+
var cancelBlock: (() -> Void)?
@objc func cancel() {
cancelBlock?()
diff --git a/UnitTests/FileDownload/Tab+WKUIDelegateTests.swift b/UnitTests/FileDownload/Tab+WKUIDelegateTests.swift
index 7fe993f1e2..c0f52330be 100644
--- a/UnitTests/FileDownload/Tab+WKUIDelegateTests.swift
+++ b/UnitTests/FileDownload/Tab+WKUIDelegateTests.swift
@@ -57,13 +57,22 @@ final class TabWKUIDelegateTests: XCTestCase {
XCTAssertNil(downloadExtensionMock.capturedSavedDownloadData)
XCTAssertNil(downloadExtensionMock.capturedMimeType)
+ let eDidCallSaveDownloadedData = expectation(description: "didCallSaveDownloadedData")
+ let c = downloadExtensionMock.$didCallSaveDownloadedData.sink { didCall in
+ if didCall {
+ eDidCallSaveDownloadedData.fulfill()
+ }
+ }
+
// WHEN
sut.webView(WKWebView(), saveDataToFile: testData, suggestedFilename: filename, mimeType: "application/pdf", originatingURL: originatingURL)
// THEN
- XCTAssertTrue(downloadExtensionMock.didCallSaveDownloadedData)
+ waitForExpectations(timeout: 1)
XCTAssertEqual(downloadExtensionMock.capturedSavedDownloadData, testData)
XCTAssertNotNil(downloadExtensionMock.capturedMimeType, "application/pdf")
+
+ withExtendedLifetime(c) {}
}
}
diff --git a/UnitTests/Fire/Model/FireTests.swift b/UnitTests/Fire/Model/FireTests.swift
index fa3bbf8efa..0afbbbbc7a 100644
--- a/UnitTests/Fire/Model/FireTests.swift
+++ b/UnitTests/Fire/Model/FireTests.swift
@@ -165,7 +165,8 @@ final class FireTests: XCTestCase {
shouldRestorePreviousSession: false)
appStateRestorationManager.applicationDidFinishLaunching()
- let fire = Fire(stateRestorationManager: appStateRestorationManager,
+ let fire = Fire(historyCoordinating: HistoryCoordinatingMock(),
+ stateRestorationManager: appStateRestorationManager,
tld: ContentBlocking.shared.tld)
XCTAssertTrue(appStateRestorationManager.canRestoreLastSessionState)
@@ -182,7 +183,8 @@ final class FireTests: XCTestCase {
shouldRestorePreviousSession: false)
appStateRestorationManager.applicationDidFinishLaunching()
- let fire = Fire(stateRestorationManager: appStateRestorationManager,
+ let fire = Fire(historyCoordinating: HistoryCoordinatingMock(),
+ stateRestorationManager: appStateRestorationManager,
tld: ContentBlocking.shared.tld)
XCTAssertTrue(appStateRestorationManager.canRestoreLastSessionState)
diff --git a/UnitTests/History/Model/HistoryCoordinatingMock.swift b/UnitTests/History/Model/HistoryCoordinatingMock.swift
index 178933c6bb..a14a6ea6c2 100644
--- a/UnitTests/History/Model/HistoryCoordinatingMock.swift
+++ b/UnitTests/History/Model/HistoryCoordinatingMock.swift
@@ -19,18 +19,23 @@
import XCTest
import BrowserServicesKit
import Common
+import History
@testable import DuckDuckGo_Privacy_Browser
final class HistoryCoordinatingMock: HistoryCoordinating {
+ func loadHistory(onCleanFinished: @escaping () -> Void) {
+ onCleanFinished()
+ }
+
var history: History?
- var allHistoryVisits: [DuckDuckGo_Privacy_Browser.Visit]?
- @Published private(set) var historyDictionary: [URL: DuckDuckGo_Privacy_Browser.HistoryEntry]?
- var historyDictionaryPublisher: Published<[URL: DuckDuckGo_Privacy_Browser.HistoryEntry]?>.Publisher { $historyDictionary }
+ var allHistoryVisits: [Visit]?
+ @Published private(set) var historyDictionary: [URL: HistoryEntry]?
+ var historyDictionaryPublisher: Published<[URL: HistoryEntry]?>.Publisher { $historyDictionary }
var addVisitCalled = false
var visit: Visit?
- func addVisit(of url: URL) -> DuckDuckGo_Privacy_Browser.Visit? {
+ func addVisit(of url: URL) -> Visit? {
addVisitCalled = true
return visit
}
diff --git a/UnitTests/History/Model/HistoryCoordinatorTests.swift b/UnitTests/History/Model/HistoryCoordinatorTests.swift
index 5f53be663d..46615a9c97 100644
--- a/UnitTests/History/Model/HistoryCoordinatorTests.swift
+++ b/UnitTests/History/Model/HistoryCoordinatorTests.swift
@@ -17,6 +17,7 @@
//
import XCTest
+import History
@testable import DuckDuckGo_Privacy_Browser
@MainActor
@@ -31,7 +32,7 @@ class HistoryCoordinatorTests: XCTestCase {
func testWhenAddVisitIsCalledBeforeHistoryIsLoadedFromStorage_ThenVisitIsIgnored() {
let historyStoringMock = HistoryStoringMock()
historyStoringMock.cleanOldResult = nil
- let historyCoordinator = HistoryCoordinator()
+ let historyCoordinator = HistoryCoordinator(historyStoring: historyStoringMock)
let url = URL.duckDuckGo
historyCoordinator.addVisit(of: url)
@@ -177,8 +178,9 @@ class HistoryCoordinatorTests: XCTestCase {
func testWhenBurningVisits_DoesntDeleteHistoryBeforeVisits() {
// Needs real store to catch assertion which can be raised by improper call ordering in the coordinator
let context = CoreData.historyStoreContainer().newBackgroundContext()
- let historyStore = HistoryStore(context: context)
+ let historyStore = EncryptedHistoryStore(context: context)
let historyCoordinator = HistoryCoordinator(historyStoring: historyStore)
+ historyCoordinator.loadHistory { }
let url1 = URL(string: "https://duckduckgo.com")!
historyCoordinator.addVisit(of: url1)
@@ -261,7 +263,7 @@ fileprivate extension HistoryCoordinator {
historyStoringMock.cleanOldResult = .success(History())
historyStoringMock.removeEntriesResult = .success(())
let historyCoordinator = HistoryCoordinator(historyStoring: historyStoringMock)
- historyCoordinator.loadHistory()
+ historyCoordinator.loadHistory { }
return (historyStoringMock, historyCoordinator)
}
diff --git a/UnitTests/History/Services/HistoryStoreTests.swift b/UnitTests/History/Services/HistoryStoreTests.swift
index b0a502bb89..2bc504a654 100644
--- a/UnitTests/History/Services/HistoryStoreTests.swift
+++ b/UnitTests/History/Services/HistoryStoreTests.swift
@@ -20,13 +20,14 @@ import XCTest
@testable import DuckDuckGo_Privacy_Browser
import Combine
import class Persistence.CoreDataDatabase
+import History
final class HistoryStoreTests: XCTestCase {
private var cancellables = Set()
private var context: NSManagedObjectContext!
- private var historyStore: HistoryStore!
+ private var historyStore: EncryptedHistoryStore!
private var location: URL!
override func setUp() {
@@ -40,7 +41,7 @@ final class HistoryStoreTests: XCTestCase {
}
}
context = database.makeContext(concurrencyType: .mainQueueConcurrencyType)
- historyStore = HistoryStore(context: context)
+ historyStore = EncryptedHistoryStore(context: context)
}
override func tearDownWithError() throws {
diff --git a/UnitTests/History/Services/HistoryStoringMock.swift b/UnitTests/History/Services/HistoryStoringMock.swift
index 8b98a3d1b9..baabb996fe 100644
--- a/UnitTests/History/Services/HistoryStoringMock.swift
+++ b/UnitTests/History/Services/HistoryStoringMock.swift
@@ -19,6 +19,7 @@
import XCTest
@testable import DuckDuckGo_Privacy_Browser
import Combine
+import History
final class HistoryStoringMock: HistoryStoring {
@@ -40,6 +41,10 @@ final class HistoryStoringMock: HistoryStoring {
}
}
+ func load() {
+ // no-op
+ }
+
var removeEntriesCalled = false
var removeEntriesArray = [HistoryEntry]()
var removeEntriesResult: Result?
@@ -72,7 +77,7 @@ final class HistoryStoringMock: HistoryStoring {
var saveCalled = false
var savedHistoryEntries = [HistoryEntry]()
- func save(entry: DuckDuckGo_Privacy_Browser.HistoryEntry) -> Future<[(id: DuckDuckGo_Privacy_Browser.Visit.ID, date: Date)], Error> {
+ func save(entry: HistoryEntry) -> Future<[(id: Visit.ID, date: Date)], Error> {
saveCalled = true
savedHistoryEntries.append(entry)
for visit in entry.visits {
diff --git a/UnitTests/Onboarding/OnboardingTests.swift b/UnitTests/Onboarding/OnboardingTests.swift
index 24c2dd4eb9..729d97636e 100644
--- a/UnitTests/Onboarding/OnboardingTests.swift
+++ b/UnitTests/Onboarding/OnboardingTests.swift
@@ -68,6 +68,7 @@ class OnboardingTests: XCTestCase {
XCTAssertEqual(0, delegate.hasFinishedCalled)
}
+ @MainActor
func testWhenSetDefaultPressedDelegateIsCalled() {
let model = OnboardingViewModel(delegate: delegate)
XCTAssertEqual(0, delegate.didRequestImportDataCalled)
diff --git a/UnitTests/Permissions/TabPermissionsTests.swift b/UnitTests/Permissions/TabPermissionsTests.swift
index d51411e6c4..c12737f67b 100644
--- a/UnitTests/Permissions/TabPermissionsTests.swift
+++ b/UnitTests/Permissions/TabPermissionsTests.swift
@@ -456,4 +456,9 @@ final class WorkspaceMock: Workspace {
return onOpen?(url) ?? false
}
+ var onOpenURLs: (([URL], String?, NSWorkspace.LaunchOptions, NSAppleEventDescriptor?, AutoreleasingUnsafeMutablePointer?) -> Bool)?
+ func open(_ urls: [URL], withAppBundleIdentifier bundleIdentifier: String?, options: NSWorkspace.LaunchOptions, additionalEventParamDescriptor descriptor: NSAppleEventDescriptor?, launchIdentifiers identifiers: AutoreleasingUnsafeMutablePointer?) -> Bool {
+ return onOpenURLs?(urls, bundleIdentifier, options, descriptor, identifiers) ?? false
+ }
+
}
diff --git a/UnitTests/Preferences/DownloadsPreferencesTests.swift b/UnitTests/Preferences/DownloadsPreferencesTests.swift
index 5d00503fed..afefd8fb8f 100644
--- a/UnitTests/Preferences/DownloadsPreferencesTests.swift
+++ b/UnitTests/Preferences/DownloadsPreferencesTests.swift
@@ -19,11 +19,13 @@
import XCTest
@testable import DuckDuckGo_Privacy_Browser
-struct DownloadsPreferencesPersistorMock: DownloadsPreferencesPersistor {
+class DownloadsPreferencesPersistorMock: DownloadsPreferencesPersistor {
+
var selectedDownloadLocation: String?
var alwaysRequestDownloadLocation: Bool
var defaultDownloadLocation: URL?
var lastUsedCustomDownloadLocation: String?
+ var shouldOpenPopupOnCompletion: Bool
var _isDownloadLocationValid: (URL) -> Bool
@@ -34,16 +36,44 @@ struct DownloadsPreferencesPersistorMock: DownloadsPreferencesPersistor {
init(
selectedDownloadLocation: String? = nil,
alwaysRequestDownloadLocation: Bool = false,
+ shouldOpenPopupOnCompletion: Bool = true,
defaultDownloadLocation: URL? = FileManager.default.temporaryDirectory,
lastUsedCustomDownloadLocation: String? = nil,
isDownloadLocationValid: @escaping (URL) -> Bool = { _ in true }
) {
self.selectedDownloadLocation = selectedDownloadLocation
self.alwaysRequestDownloadLocation = alwaysRequestDownloadLocation
+ self.shouldOpenPopupOnCompletion = shouldOpenPopupOnCompletion
self.defaultDownloadLocation = defaultDownloadLocation
self.lastUsedCustomDownloadLocation = lastUsedCustomDownloadLocation
self._isDownloadLocationValid = isDownloadLocationValid
}
+
+ func values() -> [String: any Equatable] {
+ var result = [String: any Equatable]()
+ for (label, value) in Mirror(reflecting: self).children {
+ guard let label, let value = value as? any Equatable else { continue }
+ result[label] = value
+ }
+ return result
+ }
+}
+extension [String: any Equatable] {
+ func difference(from other: Self) -> Set {
+ func areEqual(_ lhs: T, with rhs: any Equatable) -> Bool {
+ guard let rhs = rhs as? T else { return false }
+ return lhs == rhs
+ }
+
+ var result = Set(self.keys).symmetricDifference(other.keys)
+ for (key, lhs) in self {
+ guard let rhs = other[key] else { continue }
+ if !areEqual(lhs, with: rhs) {
+ result.insert(key)
+ }
+ }
+ return result
+ }
}
class DownloadsPreferencesTests: XCTestCase {
@@ -125,6 +155,36 @@ class DownloadsPreferencesTests: XCTestCase {
XCTAssertEqual(preferences.effectiveDownloadLocation, DownloadsPreferences.defaultDownloadLocation())
}
+ func testShouldOpenPopupOnCompletionSetting() {
+ let persistor1 = DownloadsPreferencesPersistorMock(shouldOpenPopupOnCompletion: true)
+ var preferences = DownloadsPreferences(persistor: persistor1)
+ XCTAssertTrue(preferences.shouldOpenPopupOnCompletion)
+
+ let persistor2 = DownloadsPreferencesPersistorMock(shouldOpenPopupOnCompletion: false)
+ preferences = DownloadsPreferences(persistor: persistor2)
+ XCTAssertFalse(preferences.shouldOpenPopupOnCompletion)
+
+ var eObjectWillChangeCalled = expectation(description: "object will change called 1")
+ let c = preferences.objectWillChange.sink {
+ eObjectWillChangeCalled.fulfill()
+ }
+
+ let valuesWithTrue = persistor1.values()
+ let valuesWithFalse = persistor2.values()
+ XCTAssertEqual(valuesWithTrue.difference(from: valuesWithFalse), ["\(\DownloadsPreferencesPersistorMock.shouldOpenPopupOnCompletion)".pathExtension])
+
+ preferences.shouldOpenPopupOnCompletion = true
+ waitForExpectations(timeout: 0)
+ XCTAssertTrue(preferences.shouldOpenPopupOnCompletion)
+ XCTAssertEqual(persistor2.values().difference(from: valuesWithTrue), [])
+
+ eObjectWillChangeCalled = expectation(description: "object will change called 2")
+ preferences.shouldOpenPopupOnCompletion = false
+ waitForExpectations(timeout: 0)
+ XCTAssertFalse(preferences.shouldOpenPopupOnCompletion)
+ XCTAssertEqual(persistor2.values().difference(from: valuesWithFalse), [])
+ }
+
private func createTemporaryTestDirectory(named name: String = DownloadsPreferencesTests.defaultTestDirectoryName) -> URL {
let baseTemporaryDirectoryURL = FileManager.default.temporaryDirectory
let testTemporaryDirectoryURL = baseTemporaryDirectoryURL.appendingPathComponent(name)
diff --git a/UnitTests/Statistics/PixelTests.swift b/UnitTests/Statistics/PixelTests.swift
index 15e2a0e27a..1e580a0652 100644
--- a/UnitTests/Statistics/PixelTests.swift
+++ b/UnitTests/Statistics/PixelTests.swift
@@ -16,10 +16,13 @@
// limitations under the License.
//
-import XCTest
+import Common
+import Networking
import OHHTTPStubs
import OHHTTPStubsSwift
-import Networking
+import PixelKit
+import XCTest
+
@testable import DuckDuckGo_Privacy_Browser
class PixelTests: XCTestCase {
@@ -39,7 +42,7 @@ class PixelTests: XCTestCase {
}
func testWhenPixelFiredThenAPIHeadersAreAdded() {
- let expectation = XCTestExpectation()
+ let expectation = expectation(description: "request sent")
stub(condition: hasHeaderNamed(userAgentName, value: testAgent)) { _ -> HTTPStubsResponse in
expectation.fulfill()
@@ -49,12 +52,11 @@ class PixelTests: XCTestCase {
let headers = APIRequest.Headers(userAgent: testAgent)
Pixel.shared!.fire(.serp, withHeaders: headers)
- wait(for: [expectation], timeout: 1.0)
-
+ waitForExpectations(timeout: 1.0)
}
func testWhenPixelIsFiredWithAdditionalParametersThenParametersAdded() {
- let expectation = XCTestExpectation()
+ let expectation = expectation(description: "request sent")
let params = ["param1": "value1", "param2": "value2"]
stub(condition: isHost(host) && isPath("/t/m_mac_crash")) { request -> HTTPStubsResponse in
@@ -66,11 +68,11 @@ class PixelTests: XCTestCase {
Pixel.fire(.crash, withAdditionalParameters: params)
- wait(for: [expectation], timeout: 1.0)
+ waitForExpectations(timeout: 1.0)
}
func testWhenErrorPixelIsFiredThenParametersAdded() {
- let expectation = XCTestExpectation()
+ let expectation = expectation(description: "request sent")
let error = NSError(domain: "TestErrorDomain", code: 42, userInfo: nil)
stub(condition: isHost(host) && isPath("/t/m_mac_debug_url")) { request -> HTTPStubsResponse in
@@ -82,31 +84,40 @@ class PixelTests: XCTestCase {
Pixel.fire(.debug(event: Pixel.Event.Debug.appOpenURLFailed, error: error))
- wait(for: [expectation], timeout: 1.0)
+ waitForExpectations(timeout: 1.0)
}
func testWhenErrorPixelIsFiredAdditionalParametersThenParametersMerged() {
- let expectation = XCTestExpectation()
+ let expectation = expectation(description: "request sent")
let params = ["param1": "value1", "d": "TheMainQuestion"]
let error = NSError(domain: "TestErrorDomain", code: 42, userInfo: ["key": 41])
- stub(condition: isHost(host) && isPath("/t/ml_mac_app-launch_as-default_app-launch")) { request -> HTTPStubsResponse in
- XCTAssertEqual("TheMainQuestion", request.url?.getParameter(named: "d"))
- XCTAssertEqual("42", request.url?.getParameter(named: "e"))
- XCTAssertEqual("value1", request.url?.getParameter(named: "param1"))
- XCTAssertEqual("value2", request.url?.getParameter(named: "param2"))
+ stub(condition: isHost(host) && isPath("/t/m_mac_debug_url")) { request -> HTTPStubsResponse in
+ var parameters = URLComponents(url: request.url!, resolvingAgainstBaseURL: false)!.queryItems!.reduce(into: [:]) { $0[$1.name] = $1.value ?? "" }
+ parameters[PixelKit.Parameters.test] = nil
+
+ XCTAssertEqual(parameters, [
+ "appVersion": AppVersion.shared.versionNumber,
+ "d": "TheMainQuestion",
+ "e": "42",
+ "param1": "value1",
+ ])
+
expectation.fulfill()
return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil)
}
Pixel.fire(.debug(event: Pixel.Event.Debug.appOpenURLFailed, error: error), withAdditionalParameters: params)
+ waitForExpectations(timeout: 1.0)
}
func testWhenPixelFiresSuccessfullyThenCompletesWithNoError() {
- let expectation = XCTestExpectation()
+ let eRequestSent = expectation(description: "request sent")
+ let expectation = expectation(description: "callback received")
stub(condition: isHost(host)) { _ -> HTTPStubsResponse in
+ eRequestSent.fulfill()
return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil)
}
@@ -115,13 +126,15 @@ class PixelTests: XCTestCase {
expectation.fulfill()
}
- wait(for: [expectation], timeout: 5.0)
+ waitForExpectations(timeout: 1.0)
}
func testWhenPixelFiresUnsuccessfullyThenCompletesWithError() {
- let expectation = XCTestExpectation()
+ let eRequestSent = expectation(description: "request sent")
+ let expectation = expectation(description: "error received")
stub(condition: isHost(host)) { _ -> HTTPStubsResponse in
+ eRequestSent.fulfill()
return HTTPStubsResponse(data: Data(), statusCode: 404, headers: nil)
}
@@ -130,7 +143,7 @@ class PixelTests: XCTestCase {
expectation.fulfill()
}
- wait(for: [expectation], timeout: 1.0)
+ waitForExpectations(timeout: 1.0)
}
}
diff --git a/UnitTests/Tab/Model/TabTests.swift b/UnitTests/Tab/Model/TabTests.swift
index faf05897e4..ed15925879 100644
--- a/UnitTests/Tab/Model/TabTests.swift
+++ b/UnitTests/Tab/Model/TabTests.swift
@@ -120,11 +120,15 @@ final class TabTests: XCTestCase {
DownloadsPreferences().alwaysRequestDownloadLocation = true
tab.webView(WebViewMock(), saveDataToFile: Data(), suggestedFilename: "anything", mimeType: "application/pdf", originatingURL: .duckDuckGo)
var expectedDialog: Tab.UserDialog?
+ let expectation = expectation(description: "savePanelDialog published")
tab.downloads?.savePanelDialogPublisher.sink(receiveValue: { userDialog in
- expectedDialog = userDialog
+ if let userDialog {
+ expectation.fulfill()
+ expectedDialog = userDialog
+ }
}).store(in: &cancellables)
- XCTAssertNotNil(expectedDialog)
+ waitForExpectations(timeout: 1)
// WHEN
tab.url = .duckDuckGoMorePrivacyInfo
diff --git a/UnitTests/Tab/Services/FaviconManagerMock.swift b/UnitTests/Tab/Services/FaviconManagerMock.swift
index 45afa591fd..70356792c6 100644
--- a/UnitTests/Tab/Services/FaviconManagerMock.swift
+++ b/UnitTests/Tab/Services/FaviconManagerMock.swift
@@ -20,6 +20,7 @@ import XCTest
import Combine
import BrowserServicesKit
import Common
+import History
@testable import DuckDuckGo_Privacy_Browser
final class FaviconManagerMock: FaviconManagement {
@@ -53,7 +54,7 @@ final class FaviconManagerMock: FaviconManagement {
}
// swiftlint:disable:next function_parameter_count
- func burnDomains(_ domains: Set, exceptBookmarks bookmarkManager: DuckDuckGo_Privacy_Browser.BookmarkManager, exceptSavedLogins: Set, exceptExistingHistory history: DuckDuckGo_Privacy_Browser.History, tld: Common.TLD, completion: @escaping () -> Void) {
+ func burnDomains(_ domains: Set, exceptBookmarks bookmarkManager: DuckDuckGo_Privacy_Browser.BookmarkManager, exceptSavedLogins: Set, exceptExistingHistory history: History, tld: Common.TLD, completion: @escaping () -> Void) {
completion()
}
}
diff --git a/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift b/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift
index 806c139493..b569bdb58d 100644
--- a/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift
+++ b/UnitTests/Tab/WKWebViewPrivateMethodsAvailabilityTests.swift
@@ -49,4 +49,10 @@ final class WKWebViewPrivateMethodsAvailabilityTests: XCTestCase {
XCTAssertEqual(pagePrefs.customHeaderFields, customHeaderFields.map { [$0] })
}
+ func testWKPDFHUDViewClassAvailable() {
+ XCTAssertNotNil(WKPDFHUDViewWrapper.WKPDFHUDViewClass)
+ XCTAssertTrue(WKPDFHUDViewWrapper.WKPDFHUDViewClass?.instancesRespond(to: WKPDFHUDViewWrapper.performActionForControlSelector) == true)
+ XCTAssertTrue(WKPDFHUDViewWrapper.WKPDFHUDViewClass?.instancesRespond(to: WKPDFHUDViewWrapper.setVisibleSelector) == true)
+ }
+
}
diff --git a/UnitTests/TabBar/Model/TabCollectionTests.swift b/UnitTests/TabBar/Model/TabCollectionTests.swift
index be6ba7be27..983091b856 100644
--- a/UnitTests/TabBar/Model/TabCollectionTests.swift
+++ b/UnitTests/TabBar/Model/TabCollectionTests.swift
@@ -17,6 +17,7 @@
//
import XCTest
+import History
@testable import DuckDuckGo_Privacy_Browser
@MainActor
@@ -187,7 +188,7 @@ extension Tab {
class HistoryTabExtensionMock: TabExtension, HistoryExtensionProtocol {
- var localHistory: [DuckDuckGo_Privacy_Browser.Visit] = []
+ var localHistory: [Visit] = []
func getPublicProtocol() -> HistoryExtensionProtocol { self }
}
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 6725154913..9fe6ea8120 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -264,6 +264,8 @@ platform :mac do
build_number: build_number
)
sh('git', 'push')
+
+ sh("echo \"release_branch_name=#{HOTFIX_BRANCH}/#{new_version}\" >> $GITHUB_OUTPUT") if is_ci
end
# Updates embedded files and pushes to remote.
diff --git a/scripts/extract_release_notes.sh b/scripts/extract_release_notes.sh
index 175cbf5ee0..9c7db812a6 100755
--- a/scripts/extract_release_notes.sh
+++ b/scripts/extract_release_notes.sh
@@ -1,29 +1,35 @@
#!/bin/bash
#
-# This script extracts release notes from Asana release task description.
+# This script extracts release notes or included tasks from Asana release task description.
#
# Usage:
-# cat release_task_description.txt | ./extract_release_notes.sh
+# cat release_task_description.txt | ./extract_release_notes.sh [-t]
#
-notes_start="release notes"
-notes_end="this release includes:"
-is_release_notes=0
-has_release_notes=0
+start_marker="release notes"
+end_marker="this release includes:"
+is_capturing=0
+has_content=0
+
+if [[ "$1" == "-t" ]]; then
+ # capture included tasks instead of release notes
+ start_marker="this release includes:"
+ end_marker=
+fi
while read -r line
do
- if [[ $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$notes_start" ]]; then
- is_release_notes=1
- elif [[ $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$notes_end" ]]; then
+ if [[ $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$start_marker" ]]; then
+ is_capturing=1
+ elif [[ -n "$end_marker" && $(tr '[:upper:]' '[:lower:]' <<< "$line") == "$end_marker" ]]; then
exit 0
- elif [[ $is_release_notes -eq 1 && -n "$line" ]]; then
- has_release_notes=1
+ elif [[ $is_capturing -eq 1 && -n "$line" ]]; then
+ has_content=1
echo "$line"
fi
done
-if [[ $has_release_notes -eq 0 ]]; then
+if [[ $has_content -eq 0 ]]; then
exit 1
fi
diff --git a/scripts/loc_export.sh b/scripts/loc_export.sh
new file mode 100755
index 0000000000..f7cee94f6a
--- /dev/null
+++ b/scripts/loc_export.sh
@@ -0,0 +1,103 @@
+#!/bin/bash
+
+# Function to display help information
+show_help() {
+ echo
+ echo "---HELP---"
+ echo "Usage: $(basename "$0") [OPTIONS]"
+ echo
+ echo "Options:"
+ echo " -n, --name NAME Specify the name for the exported xliff file"
+ echo "This script attempts to export an xliff file from the Xcode String Catalogue."
+ echo "--- ---"
+ echo
+ exit 0
+}
+
+# Check if xmlstarlet is installed
+if ! command -v xmlstarlet &> /dev/null
+then
+ echo "xmlstarlet could not be found. Please install xmlstarlet."
+ echo "You can install xmlstarlet using Homebrew:"
+ echo " brew install xmlstarlet"
+ exit 1
+fi
+
+# Parse command-line options
+while [ $# -gt 0 ]; do
+ key="$1"
+ case $key in
+ -h|--help)
+ show_help
+ ;;
+ -n|--name)
+ new_xliff_name="$2"
+ shift # past argument
+ shift # past value
+ ;;
+ *)
+ echo "Unknown option: $1"
+ show_help
+ ;;
+ esac
+done
+
+# Get the directory where the script is stored and define paths
+script_dir=$(dirname "$(readlink -f "$0")")
+export_path="${script_dir}/TempLocalizationExport"
+final_xliff_path="${script_dir}/assets/loc"
+
+# Ensure the final xliff directory exists
+mkdir -p "$final_xliff_path"
+
+# Export localizations
+xcodebuild -exportLocalizations -localizationPath "$export_path" -derivedDataPath "${script_dir}/DerivedData" -scheme "DuckDuckGo Privacy Browser" APP_STORE_PRODUCT_MODULE_NAME_OVERRIDE=DuckDuckGo_Privacy_Browser_App_Store PRIVACY_PRO_PRODUCT_MODULE_NAME_OVERRIDE=DuckDuckGo_Privacy_Browser_Privacy_Pro
+
+# Attempt to find the .xcloc package
+xcloc_package=$(find "$export_path" -type d -name "*.xcloc")
+
+# Check if .xcloc package was found and proceed
+if [ -z "$xcloc_package" ]; then
+ echo "No .xcloc package found. Exiting."
+ exit 1
+fi
+
+echo "Extracting .xliff from $xcloc_package"
+xliff_file="${new_xliff_name:-en}.xliff" # Use provided name or default to "en.xliff"
+
+# Extract the .xliff file to the final path
+cp "${xcloc_package}/Localized Contents/en.xliff" "${final_xliff_path}/${xliff_file}"
+echo "Extraction complete. .xliff file is now in ${final_xliff_path} as ${xliff_file}"
+
+# Cleanup
+echo "Cleaning up temporary files..."
+rm -rf "$export_path"
+echo "Cleanup complete."
+
+# Define an array of unwanted paths
+declare -a unwanted_paths=(
+ "DuckDuckGoDBPBackgroundAgent/Info-AppStore-InfoPlist.xcstrings"
+ "DuckDuckGoDBPBackgroundAgent/InfoPlist.xcstrings"
+ "DuckDuckGoDBPBackgroundAgent/Localizable.xcstrings"
+ "DuckDuckGoNotifications/InfoPlist.xcstrings"
+ "DuckDuckGoNotifications/Localizable.xcstrings"
+ "DuckDuckGoVPN/Info-AppStore-InfoPlist.xcstrings"
+ "DuckDuckGoVPN/InfoPlist.xcstrings"
+ "DuckDuckGoVPN/Localizable.xcstrings"
+ "VPNProxyExtension/InfoPlist.xcstrings"
+ "DuckDuckGoNotifications/Resources/InfoPlist.xcstrings"
+ "DuckDuckGo/Suggestions/View/Base.lproj/Suggestion.storyboard"
+)
+
+# Loop through each unwanted path and remove the corresponding elements
+for path in "${unwanted_paths[@]}"; do
+ echo "Removing entries for $path from the .xliff file..."
+ xmlstarlet ed --inplace -N x="urn:oasis:names:tc:xliff:document:1.2" \
+ -d "//x:file[contains(@original, '$path')]" \
+ "${final_xliff_path}/${xliff_file}"
+done
+
+echo "Modification of .xliff file complete."
+
+# Open the directory containing the xliff file
+open "${final_xliff_path}"
diff --git a/scripts/loc_import.sh b/scripts/loc_import.sh
new file mode 100755
index 0000000000..6babd5e650
--- /dev/null
+++ b/scripts/loc_import.sh
@@ -0,0 +1,97 @@
+#!/bin/sh
+
+# Function to display help information
+show_help() {
+ echo
+ echo "--- HELP ---"
+ echo "Usage: $0 [base_file_name]"
+ echo
+ echo "Arguments:"
+ echo " Mandatory. The folder containing translation files."
+ echo " [base_file_name] Optional. The base name of the translation files. If not provided, it is derived from the folder name."
+ echo
+ echo "This script processes .xliff files in the specified translation folder."
+ echo "It updates 'target-language' attributes and changes state from 'new' to 'translated'."
+ echo "Finally, it attempts to import the processed .xliff files into an Xcode project."
+ echo "--- ---"
+ echo
+}
+
+# Parse command-line options
+while [ $# -gt 0 ]; do
+ key="$1"
+ case $key in
+ -h|--help)
+ show_help
+ exit 0
+ ;;
+ *)
+ break
+ ;;
+ esac
+done
+
+# Get the directory where the script is stored
+script_dir=$(dirname "$(readlink -f "$0")")
+base_dir="${script_dir}/.."
+
+input_path=$1
+baseName=$2
+
+# Check for the presence of at least one argument
+if [ $# -lt 1 ]; then
+ show_help
+ exit 1
+fi
+
+# Check for the presence of two arguments if second not provided the base file name is derived from the folder name."
+if [ $# -lt 2 ]; then
+ baseName=$(basename "$input_path" .zip)
+ baseName=$(basename "$baseName" .xliff)
+ echo "Choosing ${baseName} as a base name for translation files"
+fi
+
+# If input is zip file then extract it
+if expr "$input_path" : '.*\.zip$' > /dev/null; then
+ extraction_dir="$(dirname "$input_path")/$baseName"
+ mkdir -p "$extraction_dir"
+ echo "Unzipping $input_path into $extraction_dir"
+ unzip -o "$input_path" -d "$extraction_dir"
+ input_path="$extraction_dir"
+fi
+
+for dir in "$input_path"/*; do
+ echo "Processing ${dir}"
+ locale=$(basename "${dir}")
+ targetLocale=$(echo "${locale}" | cut -f1 -d-)
+
+ if test -f "${dir}/${baseName}.xliff"; then
+ echo "Processing ${locale} xliff"
+ fileName="${baseName}.xliff"
+ if [ "${locale}" != "${targetLocale}" ]; then
+ echo "Changing locale from '$locale' to '$targetLocale'"
+ # Modify the target-language attribute
+ sed -i '.bak' "s/target-language=\"${locale}\"/target-language=\"${targetLocale}\"/" "${dir}/${fileName}"
+ rm "${dir}/${fileName}.bak"
+ fi
+
+ # Change state="new" to state="translated"
+ echo "Changing state from 'new' to 'translated' for all entries in ${fileName}"
+ sed -i '.bak' 's/state="new"/state="translated"/g' "${dir}/${fileName}"
+ rm "${dir}/${fileName}.bak"
+
+ echo "Importing ${dir}/${fileName} ..."
+
+ if ! xcodebuild -importLocalizations -project "${base_dir}/DuckDuckGo.xcodeproj" -localizationPath "${dir}/${fileName}" APP_STORE_PRODUCT_MODULE_NAME_OVERRIDE="DuckDuckGo_Privacy_Browser_App_Store" PRIVACY_PRO_PRODUCT_MODULE_NAME_OVERRIDE="DuckDuckGo_Privacy_Browser_Privacy_Pro"; then
+ echo "ERROR: Failed to import ${dir}/${fileName}"
+ echo
+ echo "Check translation folder and files then try again."
+ echo
+ exit 1
+ fi
+ else
+ echo "ERROR: ${fileName} xliff not found in ${dir}"
+ echo
+ exit 1
+ fi
+done
diff --git a/scripts/update_asana_for_release.sh b/scripts/update_asana_for_release.sh
index 6f70c994e7..e59c9f97ed 100755
--- a/scripts/update_asana_for_release.sh
+++ b/scripts/update_asana_for_release.sh
@@ -3,15 +3,19 @@
# This scripts updates Asana tasks related to the release:
# - Updates "This release includes:" section of the release task with the list
# of Asana tasks linked in git commit messages since the last official release tag.
-# - Moves all tasks (including the release task itself) to the Validation section
-# in macOS App Board project.
+# - Moves all tasks (including the release task itself) to the section
+# in macOS App Board project identified by the target-section-id argument
+# (Validation for internal releases, Done for public/hotfix releases).
# - Tags all tasks with the release tag (creating the tag as needed).
+# - Closes all tasks that don't require a post-mortem, based on the following criteria:
+# - Task does not belong to Current Objectives project
+# - Task is not a subtask of SRE for Native Apps Engineering task (where incidents are kept)
#
# Note: this script is intended to be run in CI environment and should not
# be run locally as part of the release process.
#
# Usage:
-# ./update_asana_for_release.sh
+# ./update_asana_for_release.sh [announcement-task-contents-file]
#
set -e -o pipefail
@@ -19,6 +23,8 @@ set -e -o pipefail
workspace_id="137249556945"
asana_api_url="https://app.asana.com/api/1.0"
task_url_regex='^https://app.asana.com/[0-9]/[0-9]*/([0-9]*)/f$'
+default_incidents_parent_task_id="1135688560894081"
+default_current_objectives_project_id="72649045549333"
cwd="$(dirname "${BASH_SOURCE[0]}")"
find_task_urls_in_git_log() {
@@ -53,11 +59,10 @@ get_task_id() {
fi
}
-construct_task_description() {
+construct_release_notes() {
local escaped_release_note
- printf '%s' "Note: This task's description is managed automatically. \n"
- printf '%s' 'Only the Release notes section below should be modified manually.\n'
- printf '%s' 'Please do not adjust formatting.Release notes '
+
+ printf '%s' 'Release notes '
if [[ -n "${release_notes[*]}" ]]; then
printf '%s' ''
for release_note in "${release_notes[@]}"; do
@@ -66,23 +71,57 @@ construct_task_description() {
done
printf '%s' ' '
fi
+}
+construct_this_release_includes() {
printf '%s' 'This release includes: '
if [[ -n "${task_ids[*]}" ]]; then
printf '%s' ''
for task_id in "${task_ids[@]}"; do
- printf '%s' " "
+ if [[ "$task_id" != "$release_task_id" ]]; then
+ printf '%s' " "
+ fi
done
printf '%s' ' '
fi
+}
+
+construct_release_task_description() {
+ printf '%s' "Note: This task's description is managed automatically. \n"
+ printf '%s' 'Only the Release notes section below should be modified manually.\n'
+ printf '%s' 'Please do not adjust formatting.'
+
+ construct_release_notes
+ construct_this_release_includes
printf '%s' ''
}
+construct_release_announcement_task_description() {
+ printf '%s' "As the last step of the process, post a message to REVIEW / RELEASE Asana project:"
+ printf '%s' ''
+ printf '%s' "Set the title to macOS App Release ${marketing_version} "
+ printf '%s' 'Copy the content below (between separators) and paste as the message body. '
+ printf '%s' ' \n '
+
+ construct_release_notes
+ printf '%s' '\n'
+ construct_this_release_includes
+ printf '%s' '\n'
+
+ printf '%s' 'Rollout \n'
+ printf '%s' 'This is now rolling out to users. New users will receive this release immediately, '
+ printf '%s' 'existing users will receive this gradually over the next few days. You can force an update now '
+ printf '%s' 'by going to the DuckDuckGo menu in the menu bar and selecting "Check For Updates".'
+ printf '%s' ' '
+ printf '%s' ''
+}
+
update_task_description() {
local html_notes="$1"
- local request_payload="{\"data\":{\"html_notes\":\"${html_notes}\"}}"
+ local escaped_html_notes=${html_notes//\"/\\\"}
+ local request_payload="{\"data\":{\"html_notes\":\"${escaped_html_notes}\"}}"
curl -fLSs -X PUT "${asana_api_url}/tasks/${release_task_id}?opt_fields=permalink_url" \
-H 'Content-Type: application/json' \
@@ -96,15 +135,17 @@ move_tasks_to_section() {
local task_ids=("$@")
for task_id in "${task_ids[@]}"; do
+ printf '%s' "Moving task $task_id to section $section_id ..."
curl -fLSs "${asana_api_url}/sections/${section_id}/addTask" \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \
--output /dev/null \
-d "{\"data\": {\"task\": \"${task_id}\"}}"
+ echo 'β
'
done
}
-find_or_create_asana_release_tag() {
+find_asana_release_tag() {
local marketing_version="$1"
local tag_name="macos-app-release-${marketing_version}"
local tag_id
@@ -113,6 +154,16 @@ find_or_create_asana_release_tag() {
-H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \
| jq -r ".data[] | select(.name==\"${tag_name}\").gid")"
+ echo "$tag_id"
+}
+
+find_or_create_asana_release_tag() {
+ local marketing_version="$1"
+ local tag_name="macos-app-release-${marketing_version}"
+ local tag_id
+
+ tag_id="$(find_asana_release_tag "$marketing_version")"
+
if [[ -z "$tag_id" ]]; then
tag_id=$(curl -fLSs "${asana_api_url}/workspaces/${workspace_id}/tags?opt_fields=gid" \
-H 'Content-Type: application/json' \
@@ -137,38 +188,110 @@ tag_tasks() {
done
}
-main() {
- local release_task_id="$1"
- local marketing_version="$2"
- local validation_section_id="$3"
+fetch_tagged_tasks_ids() {
+ local tag_id="$1"
+ local url="${asana_api_url}/tags/${tag_id}/tasks?opt_fields=gid&limit=100"
+ local response
+ local tasks_list
+ local task_ids=()
- if [[ -z "$release_task_id" ]]; then
- echo "Usage: $0 "
- exit 1
- fi
+ while true; do
+ response=$(curl -fLSs "$url" -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}")
+ tasks_list="$(jq -r .data[].gid <<< "$response")"
+ url="$(jq -r .next_page.uri <<< "$response")"
+
+ while read -r line; do
+ task_ids+=("$line")
+ done <<< "$tasks_list"
+
+ if [[ "$url" == "null" ]]; then
+ break
+ fi
+ done
+
+ echo "${task_ids[@]}"
+}
+
+fetch_incident_task_ids() {
+ local url="${asana_api_url}/tasks/${incidents_parent_task_id}/subtasks?opt_fields=gid&limit=100"
+ local response
+ local tasks_list
+ local task_ids=()
+
+ while true; do
+ response=$(curl -fLSs "$url" -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}")
+ tasks_list="$(jq -r .data[].gid <<< "$response")"
+ url="$(jq -r .next_page.uri <<< "$response")"
+ while read -r line; do
+ task_ids+=("$line")
+ done <<< "$tasks_list"
+
+ if [[ "$url" == "null" ]]; then
+ break
+ fi
+ done
+
+ echo "${task_ids[@]}"
+}
+
+complete_tasks() {
+ local task_ids=("$@")
+
+ # 1. Fetch incident task IDs (subtasks of the incidents umbrella task)
+ local incident_task_ids
+ read -ra incident_task_ids <<< "$(fetch_incident_task_ids)"
+
+ for task_id in "${task_ids[@]}"; do
+ if [[ "$task_id" == "$release_task_id" ]]; then
+ continue
+ fi
+
+ # 2. Check if task is in Current Objectives project
+ local is_in_current_objectives_project
+ is_in_current_objectives_project="$(curl -fLSs "${asana_api_url}/tasks/${task_id}/projects?opt_fields=gid" \
+ -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \
+ | jq -r ".data[] | select(.gid == \"${current_objectives_project_id}\").gid")"
+
+ # 3. If not in Current Objectives and not an incident task, mark as completed
+ # shellcheck disable=SC2076
+ if [[ -z "$is_in_current_objectives_project" ]] && ! [[ "${incident_task_ids[*]}" =~ "$task_id" ]]; then
+ printf '%s' "Closing task $task_id ..."
+ curl -X PUT -fLSs "${asana_api_url}/tasks/${task_id}" \
+ -H "Authorization: Bearer ${ASANA_ACCESS_TOKEN}" \
+ -H 'content-type: application/json' \
+ --output /dev/null \
+ -d '{"data": {"completed": true}}'
+ echo 'β
'
+ else
+ echo "Not closing task $task_id because it's a Current Objective or an incident task"
+ fi
+ done
+}
+
+handle_internal_release() {
# 1. Fetch task URLs from git commit messages
- task_ids=()
+ local task_ids=()
while read -r line; do
task_ids+=("$(get_task_id "$line")")
done <<< "$(find_task_urls_in_git_log)"
# 2. Fetch current release notes from Asana release task.
- release_notes=()
+ local release_notes=()
while read -r line; do
release_notes+=("$line")
done <<< "$(fetch_current_release_notes "${release_task_id}")"
# 3. Construct new release task description
local html_notes
- html_notes="$(construct_task_description)"
+ html_notes="$(construct_release_task_description)"
# 4. Update release task description
update_task_description "$html_notes"
# 5. Move all tasks (including release task itself) to the validation section
task_ids+=("${release_task_id}")
- move_tasks_to_section "$validation_section_id" "${task_ids[@]}"
+ move_tasks_to_section "$target_section_id" "${task_ids[@]}"
# 6. Get the existing Asana tagΒ for the release, or create a new one.
local tag_id
@@ -178,4 +301,56 @@ main() {
tag_tasks "$tag_id" "${task_ids[@]}"
}
+handle_public_release() {
+ local incidents_parent_task_id="${INCIDENTS_PARENT_TASK_ID:-${default_incidents_parent_task_id}}"
+ local current_objectives_project_id="${CURRENT_OBJECTIVES_PROJECT_ID:-${default_current_objectives_project_id}}"
+
+ # 1. Get the existing Asana tagΒ for the release.
+ local tag_id
+ tag_id=$(find_asana_release_tag "$marketing_version")
+
+ # 2. Fetch task IDs for the release tag.
+ local task_ids
+ read -ra task_ids <<< "$(fetch_tagged_tasks_ids "$tag_id")"
+
+ # 3. Move all tasks to Done section.
+ move_tasks_to_section "$target_section_id" "${task_ids[@]}"
+
+ # 4. Complete tasks that don't require a post-mortem.
+ complete_tasks "${task_ids[@]}"
+
+ # 5. Fetch current release notes from Asana release task.
+ local release_notes=()
+ while read -r line; do
+ release_notes+=("$line")
+ done <<< "$(fetch_current_release_notes "${release_task_id}")"
+
+ # 6. Construct release announcement task description
+ local html_notes
+ html_notes="$(construct_release_announcement_task_description)"
+ cat > "${announcement_task_contents_file}" <<< "${html_notes}"
+}
+
+main() {
+ local release_type="$1"
+ local release_task_id="$2"
+ local target_section_id="$3"
+ local marketing_version="$4"
+
+ case "$release_type" in
+ internal)
+ handle_internal_release
+ ;;
+ public | hotfix)
+ local announcement_task_contents_file="$5"
+ handle_public_release
+ ;;
+ *)
+ echo "Invalid release type: ${release_type}" >&2
+ exit 1
+ ;;
+ esac
+
+}
+
main "$@"
\ No newline at end of file