diff --git a/.github/workflows/analyze-test.yaml b/.github/workflows/analyze-test.yaml index 94d7656e2a..7485a81c1e 100644 --- a/.github/workflows/analyze-test.yaml +++ b/.github/workflows/analyze-test.yaml @@ -1,5 +1,10 @@ on: - workflow_call: + pull_request: + paths-ignore: + - ".github/**" + - "docs" + - "Jenkinsfile" + - "**/*.md" name: Analyze and test @@ -25,7 +30,7 @@ jobs: - name: Setup flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.0.5" + flutter-version: "3.10.6" channel: "stable" cache: true cache-key: "deps-${{ hashFiles('**/pubspec.lock') }}" @@ -56,22 +61,3 @@ jobs: with: name: test-reports path: test-report*.json - - report: - runs-on: ubuntu-latest - if: success() || failure() # Always upload report - needs: - - analyze-test - steps: - - uses: actions/checkout@v3 - - - uses: actions/download-artifact@v3 - with: - name: test-reports - - - uses: dorny/test-reporter@v1 - with: - name: Flutter Tests - path: "*.json" - reporter: flutter-json - only-summary: "true" diff --git a/.github/workflows/ci.yaml b/.github/workflows/build.yaml similarity index 52% rename from .github/workflows/ci.yaml rename to .github/workflows/build.yaml index 45e7b58ed6..6410da61cc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/build.yaml @@ -1,23 +1,11 @@ on: workflow_dispatch: - pull_request: - paths-ignore: - - ".github/**" - - "docs" - - "Jenkinsfile" - - "**/*.md" -name: CI +name: Build dev binaries jobs: - analyze-test: - name: Analyze and test - uses: ./.github/workflows/analyze-test.yaml - build-app: name: Build app - needs: - - analyze-test runs-on: ${{ matrix.runner }} strategy: matrix: @@ -35,27 +23,24 @@ jobs: - name: Setup flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.0.5" + flutter-version: "3.10.6" channel: "stable" cache: true cache-key: deps-${{ hashFiles('**/pubspec.lock') }} # optional, change this to force refresh cache cache-path: ${{ runner.tool_cache }}/flutter # optional, change this to specify the cache path + - name: Setup Fastlane + uses: ruby/setup-ruby@v1 + with: + ruby-version: "ruby" + bundler-cache: true + working-directory: ${{ matrix.os }} + - name: Setup Firebase env env: FIREBASE_ENV: ${{ secrets.FIREBASE_ENV }} run: echo "$FIREBASE_ENV" > ./configurations/env.fcm - - name: Setup Android environment - if: matrix.os == 'android' - env: - PLAY_STORE_UPLOAD_KEY_BASE64: ${{ secrets.PLAY_STORE_UPLOAD_KEY_BASE64 }} - PLAY_STORE_KEY_INFO_BASE64: ${{ secrets.PLAY_STORE_KEY_INFO_BASE64 }} - run: | - echo "$PLAY_STORE_UPLOAD_KEY_BASE64" | base64 --decode > app/keystore.jks - echo "$PLAY_STORE_KEY_INFO_BASE64" | base64 --decode > key.properties - working-directory: ${{ matrix.os }} - - name: Setup Java if: matrix.os == 'android' uses: actions/setup-java@v3 @@ -65,37 +50,25 @@ jobs: - name: Setup iOS environment if: matrix.os == 'ios' - env: - CERTIFICATE_BASE64: ${{ secrets.CERTIFICATE_BASE64 }} - PROVISION_PROFILE_BASE64: ${{ secrets.PROVISION_PROFILE_BASE64 }} - SHAREEXT_PROVISION_PROFILE_BASE64: ${{ secrets.SHAREEXT_PROVISION_PROFILE_BASE64 }} run: | - echo -n "$CERTIFICATE_BASE64" | base64 --decode --output cert.p12 - echo -n "$PROVISION_PROFILE_BASE64" | base64 --decode --output buildpp.mobileprovision - echo -n "$SHAREEXT_PROVISION_PROFILE_BASE64" | base64 --decode --output shareextpp.mobileprovision - flutter pub get && pod install + flutter pub get + pod install && pod update working-directory: ${{ matrix.os }} - - name: Setup Fastlane - uses: ruby/setup-ruby@v1 - with: - ruby-version: "ruby" - bundler-cache: true - working-directory: ${{ matrix.os }} - - name: Run prebuild run: bash prebuild.sh - name: Build env: - CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + APPLE_CERTIFICATES_SSH_KEY: ${{ secrets.APPLE_CERTIFICATES_SSH_KEY }} run: bundle exec fastlane dev working-directory: ${{ matrix.os }} - name: Upload artifacts uses: actions/upload-artifact@v3 with: - name: tmail-dev-pr-${{ github.event.pull_request.number }} + name: tmail-dev path: | - build/app/outputs/flutter-apk/app-release.apk + build/app/outputs/flutter-apk/app-debug.apk ios/Runner.ipa diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml new file mode 100644 index 0000000000..daf2a00c5b --- /dev/null +++ b/.github/workflows/image.yaml @@ -0,0 +1,98 @@ +on: + push: + branches: + - "master" + tags: + - "v*.*.*" + +name: Build Docker images + +jobs: + build-dev-image: + name: Build development image + if: github.ref_type == 'branch' && github.ref_name == 'master' + runs-on: ubuntu-latest + environment: dev + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ github.repository_owner }}/tmail-web + ghcr.io/${{ github.repository_owner }}/tmail-web + tags: | + type=ref,event=branch + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push image + uses: docker/build-push-action@v4 + with: + push: true + cache-from: | + type=gha + cache-to: | + type=gha + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + build-release-image: + name: Build release image + if: github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + environment: prod + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ${{ github.repository_owner }}/tmail-web + ghcr.io/${{ github.repository_owner }}/tmail-web + tags: | + type=ref,event=tag + type=raw,value=release + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push image + uses: docker/build-push-action@v4 + with: + push: true + cache-from: | + type=gha + cache-to: | + type=gha + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/pr-deployments.yaml b/.github/workflows/pr-deployments.yaml new file mode 100644 index 0000000000..43cf467cb2 --- /dev/null +++ b/.github/workflows/pr-deployments.yaml @@ -0,0 +1,69 @@ +on: + pull_request: + paths: + - "**/*.dart" + +name: Deploy PR on Github Pages + +jobs: + deploy: + name: Build web version and deploy + runs-on: ubuntu-latest + environment: + name: PR-${{ github.event.pull_request.number }} + url: ${{ steps.configure.outputs.URL }} + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: "3.10.6" + channel: "stable" + cache: true + cache-key: deps-${{ hashFiles('**/pubspec.lock') }} # optional, change this to force refresh cache + cache-path: ${{ runner.tool_cache }}/flutter # optional, change this to specify the cache path + + - name: Run prebuild + run: bash prebuild.sh + + - name: Configure environments + id: configure + env: + FOLDER: ${{ github.event.pull_request.number }} + run: | + sed -i "s|SERVER_URL=.*|SERVER_URL=https://apisix.upn.integration-open-paas.org/|g" env.file + sed -i "s|DOMAIN_REDIRECT_URL=.*|DOMAIN_REDIRECT_URL=https://$GITHUB_REPOSITORY_OWNER.github.io/${GITHUB_REPOSITORY##*/}/$FOLDER|g" env.file + echo "URL=https://$GITHUB_REPOSITORY_OWNER.github.io/${GITHUB_REPOSITORY##*/}/$FOLDER" >> $GITHUB_OUTPUT + + - name: Build + env: + FOLDER: ${{ github.event.pull_request.number }} + run: flutter build web --profile --verbose --base-href "/${GITHUB_REPOSITORY##*/}/$FOLDER/" + + - name: Deploy to Github Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + destination_dir: ${{ github.event.pull_request.number }} + keep_files: true + publish_dir: "build/web" + + - name: Find deployment comment + uses: peter-evans/find-comment@v2 + id: fc + with: + comment-author: "github-actions[bot]" + issue-number: ${{ github.event.pull_request.number }} + body-includes: "This PR has been deployed to" + + - name: Create or update deployment comment + uses: peter-evans/create-or-update-comment@v3 + with: + comment-id: ${{ steps.fc.outputs.comment-id }} + issue-number: ${{ github.event.pull_request.number }} + body: | + This PR has been deployed to ${{ steps.configure.outputs.URL }}. + edit-mode: replace diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ee29039ceb..f14ce8f1ca 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -6,15 +6,9 @@ on: name: Release jobs: - analyze-test: - name: Analyze and test - uses: ./.github/workflows/analyze-test.yaml - release: name: Release if: startsWith(github.ref, 'refs/tags/v') - needs: - - analyze-test runs-on: ${{ matrix.runner }} strategy: matrix: @@ -34,7 +28,7 @@ jobs: - name: Setup flutter uses: subosito/flutter-action@v2 with: - flutter-version: "3.0.5" + flutter-version: "3.10.6" channel: "stable" cache: true cache-key: deps-${{ hashFiles('**/pubspec.lock') }} # optional, change this to force refresh cache @@ -45,6 +39,20 @@ jobs: FIREBASE_ENV: ${{ secrets.FIREBASE_ENV }} run: echo "$FIREBASE_ENV" > ./configurations/env.fcm + - name: Setup Fastlane + uses: ruby/setup-ruby@v1 + with: + ruby-version: "ruby" + bundler-cache: true + working-directory: ${{ matrix.os }} + + - name: Setup Java + if: matrix.os == 'android' + uses: actions/setup-java@v3 + with: + distribution: "temurin" + java-version: "11" + - name: Setup Android environment if: matrix.os == 'android' env: @@ -55,44 +63,23 @@ jobs: echo "$PLAY_STORE_KEY_INFO_BASE64" | base64 --decode > key.properties working-directory: ${{ matrix.os }} - - name: Setup Java - if: matrix.os == 'android' - uses: actions/setup-java@v3 - with: - distribution: "temurin" - java-version: "11" - - name: Setup iOS environment if: matrix.os == 'ios' - env: - CERTIFICATE_BASE64: ${{ secrets.CERTIFICATE_BASE64 }} - PROVISION_PROFILE_BASE64: ${{ secrets.PROVISION_PROFILE_BASE64 }} - SHAREEXT_PROVISION_PROFILE_BASE64: ${{ secrets.SHAREEXT_PROVISION_PROFILE_BASE64 }} - APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} run: | - echo -n "$CERTIFICATE_BASE64" | base64 --decode --output cert.p12 - echo -n "$PROVISION_PROFILE_BASE64" | base64 --decode --output buildpp.mobileprovision - echo -n "$SHAREEXT_PROVISION_PROFILE_BASE64" | base64 --decode --output shareextpp.mobileprovision - echo -n "$APPLE_API_KEY_BASE64" | base64 --decode --output apiKey.p8 - flutter pub get && pod install + flutter pub get + pod install && pod update working-directory: ${{ matrix.os }} - - name: Setup Fastlane - uses: ruby/setup-ruby@v1 - with: - ruby-version: "ruby" - bundler-cache: true - working-directory: ${{ matrix.os }} - - name: Run prebuild run: bash prebuild.sh - name: Build and deploy env: - CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }} - FASTLANE_USER: ${{ secrets.APPLE_ID }} + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + APPLE_CERTIFICATES_SSH_KEY: ${{ secrets.APPLE_CERTIFICATES_SSH_KEY }} PLAY_STORE_CONFIG_JSON: ${{ secrets.PLAY_STORE_CONFIG_JSON }} APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }} + APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }} run: bundle exec fastlane release working-directory: ${{ matrix.os }} diff --git a/.github/workflows/test-reports.yaml b/.github/workflows/test-reports.yaml new file mode 100644 index 0000000000..25b52c69d2 --- /dev/null +++ b/.github/workflows/test-reports.yaml @@ -0,0 +1,21 @@ +on: + workflow_run: + workflows: + - "Analyze and test" + types: + - completed + +name: Test Reports + +jobs: + reports: + name: Upload test reports + runs-on: ubuntu-latest + steps: + - uses: dorny/test-reporter@v1 + with: + artifact: test-reports + name: Flutter Tests + path: "*.json" + reporter: flutter-json + only-summary: "true" diff --git a/.gitignore b/.gitignore index b475f4f6a3..77a53b29b0 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,4 @@ app.*.symbols #generated file *.g.dart messages_*.dart +*.mocks.dart diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000..fe6b2e4c9e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,283 @@ +## [0.9.3] - 2023-09-19 +### Fixed +- \[HOT-FIX\] Fix built release but nginx route 404 not found on web + +## [0.9.2] - 2023-09-14 +### Changed +- \#2134 \[WEB\] mailto URL +- Translate Russian/French/German +- \#2124 Add badges for downloads (#2140) +- \#2123 Change license to AGPL-V3 - drop the OpenPaaS clause + +### Fixed +- \#1844 Fix \[AdvanceSearch\] User cannot get the advance searching result once user use Enter without the clicking Search button +- \#1845 Fix \[AdvanceSearch\] The searching result is not correct with the condition which mention in the description +- \#1977 Fix clickable logo +- \#1984 Fix \[Barcamp\] Counter for Trash/Spam + empty action for Trash/Spam +- \#2026 Fix \[COMPOSER\] Save as draft should not close the composer +- \#2129 Fix \[Attach image\] I cannot attach image +- \#2135 Add Splash Screen for user to prevent blank page in the first time loading TeamMail app +- \#2089 Add dash-dash-space to signature delimiter +- Fix the login screen freezes when pasting the link in the browser address bar +- Fix TextEditingController was used after being disposed in LoginView +- Fix oidc refresh token on mobile + +## [0.9.1] - 2023-08-23 +### Fixed +- \#1974 Fix refreshToken with OIDC on jmap.linagora.com/oidc +- \#2099 Fix quota information are missing + +## [0.9.0] - 2023-08-18 +### Added +- \#1710 Delete all spam emails +- \#2064 Display banner when the quota reaches the limit +- \#2078 Add calendar Yes/No/Maybe options + +### Changed +- Upgrade flutter version to 3.10.6 + +### Fixed +- \#2047 Can not see the link when type is text/plain in email view +- \#1981 Spam banner is too big +- \#2066 Calendar not apply on version 0.8.9 when upgrade from old version +- \#2067 Disable swipe left/right to next/previous email in email view +- \#2068 Calendar banner widget is gray bar +- Change SERVER\_URL to deployment PR success +- \#1982 Change mailboxes label to folder +- \#1714 Refreshing mailbox: display an animation while loading +- \#2087 \[IOS/ ANDROID\] The Status Bar is missing when user change Dark theme mode in device system +- \#1983 Create folder - How to create filter easier +- \#2089 Fix pull-to-refresh in mailbox view +- \#2092 Signature delimiter should be dash - dash - space +- \#1961 Email text content could be temporarily truncated + +## [0.8.9] - 2023-07-31 +### Added +- Translate Russian and French +- Apply new view calendar event + +### Fixed +- \#2052 Fix TeamMailbox email address alignment is incorrect +- If no refreshToken return, maybe the old refreshToken still available + +## [0.8.8] - 2023-07-20 +### Fixed +- \#2046 App crashes when login account information is incorrect on web + +## [0.8.7] - 2023-07-19 +### Fixed +- \#1912 Fix rendering issue on TMail when reading an email +- Fix re-login app when token expires in OIDC + +## [0.8.6] - 2023-07-14 +### Added +- \#1486 Support inserting images in identity + +### Fixed +- \#1868 Hide parent - show child: nothing displayed in the side bad +- \#1898 There is no notification badge only on IOS +- \#1985 Hoover to see the selection +- \#1993 Has attachment checkbox is overflow once it be translated in russia +- \#1994 Long press to copy an email address +- \#2008 Instead of toasting for network connection, we should show a very small UI at the top of the list +- \#2030 Turn off composer logs by defaults + +## [0.8.5] - 2023-07-14 +### Added +- Translation (Arabic/Russian) + +### Fixed +- \#1683 Remove Plain text option of message in VacationView +- \#1895 \[Animation\] The screen seem be black before loading the email content successfully + +## [0.8.4] - 2023-07-07 +### Fixed +- \#1932 Turn off notifications after emailing +- \#1963 Bypass the Screen with button Single sign-on when OIDC flow is detected +- \#1829 App crash upon 401 +- \#1933 The inbox and sending queue is highlight once some emails in sending queue was sent in the same time +- \#1877 \[Composer\] Text Style is not changed correctly once user change the focus +- \#1952 Display signature in composer on mobile +- \#1974 RefreshToken with OIDC on jmap.linagora.com/oidc +- \#1708 \[UI\] \[Change languages\] Translating system mailboxes (INBOX, etc…) +- \#1957 \[RTL\] Email subjects are displayed overlap email content +- \#1957 \[RTL\] Name of attachments are reversed +- \#1958 \[RTL\] 'To' and 'Cc' fields are not displayed in the single line +- \#1959 \[RTL\] Invalid email red border is displayed over the address field +- \#1960 \[RTL\] Cannot edit signature in Profile Identity + +## [0.8.3] - 2023-06-27 +### Fixed +- \#1878 Fix font family not changed correctly in composer +- \#1913 Fix Email/changes is called multiple times when an error cannotCalculateChanges is returned +- \#1931 Fix user cannot do infinity scroll when he turn off network and reconnect again +- \#1923 Fix user cannot view that email content by click on the notification in Offline Mode + +## [0.8.2] - 2023-06-15 +### Fixed +- Fix select font style always default when changed +- Fix attachments not show when click ShowAll button +- Fix alignment delete button in attachment item file of email view in RTL mode +- Hide keyboard when open choose attachment dialog on mobile + +## [0.8.1] - 2023-06-15 +### Added +- Support RTL mode +- Dockerfile - Add new stage for minify js file +- \[Docs\] Configure OIDC +- Support configuration for OIDC scopes + +### Fixed +- Fix disable connected network toast notification + +## [0.8.0] - 2023-06-12 +### Added +- \#1786 Write stories for swipe in email item in ThreadView +- Support Sending emails when offline +- Support manage the sending queue +- Support view/modify SendingQueue entries + +### Fixed +- \#1862 Fix right click on email item in email list view +- \#1851 Fix \[COMPOSER\] Selected rich text menu do not match current text status +- \#1851 Fix \[COMPOSER\] Need to apply color twice for bold + +## [0.7.11] - 2023-06-05 +### Changed +- Upgrade version flutter-typeahead + +## [0.7.10] - 2023-06-05 +### Added +- Keep N recently opened email in the cache +- Reading recent emails when on slow network/offline +- Config work manager on mobile + +### Changed +- Split the test report job for forks + +### Fixed +- Force browsers to re-validate cache before reuse + +## [0.7.9] - 2023-05-08 +### Added +- \#1792 Add option 'Empty Trash' in folder menu + +### Changed +- \#1755 Use TupleKey('ObjectId\|AccountId\|UserName') store data to cache + +### Fixed +- \#1385 Fix \[Email rule\] Icon edit and delete might be seen as disable +- \#1677 Fix \[ManageAccount\]\[Forwarding\] Email validation is not working well +- \#1687 Fix \[UI\] Unread counter and mailbox name are displayed not in single line +- \#1735 Fix filters not applied to search +- \#1743 Fix horizontal scroll bar in Email detail view +- \#1798 Fix \[Toast\] Error message is displayed behind the keyboard on IOS platform only +- \#1803 Fix advanced search can't clear 'Have the world' +- \#1806 Fix impossible to disable used fonts in email composer + +## [0.7.8] - 2023-04-24 +### Added +- Added workflow to create a deployment on PR + +### Fixed +- \#1711 Fix the label is displayed overlap the button in France, Italian, Russia languages +- \#1740 Fix double scrollbar in composer web +- \#1759 Fix reply all do not always include me in recipients +- \#1763 Fix duplicated email suggestion in the list +- \#1767 Fix Reply/Forward: original message should be marked, not the sent one +- \#1778 Fix system not display signature in email which has been sent +- \#1779 Fix logic of replacing dot in long email + +## [0.7.7] - 2023-04-14 +### Added +- \#1599 Remove notification when read and delete permanent email +- \#1100 Display Answered / Forwarded keywords +- \#1600 No notification for content of Draft, Sent, Outbox, email already seen + +### Changed +- \#1598 Dismiss should mark emails in the spam box as seen + +### Fixed +- \#1613 When network is down, Tmail shouldn't prompt for a password +- \#1699 Support hide suggestion when scrolling list email to, cc, bcc. +- \#1681 Add a dot at the end of the description +- \#1687 Set name email and counter displayed in a single line +- \#1685 Fix hover and click to button show all or hide attachments +- \#1694 The Cancel/Save button is hidden in IdentityCreatorView +- \#1688 \[Identity\] \[Crashed\] Tmail UI is broken after user create identity successfully by html style +- \#1684 \[Identity\] User click on blank area but system redirect user to 404 not found page +- \#1679 \[Identity\] Button 'X' : Sometime it's hidden, sometime it's displayed +- \#1693 \[Identity\] There are 2 cursors on Create Identity screen +- \#1667 \[Energy-Economy\] Do not request spam on every email I move +- \#1698 \[Mailbox\] System not redirect correct url when user double click many times to view email in mailbox +- \#1677 \[ManageAccount\]\[Fowarding\] Email validation is not working well +- \#1655 Add a line break after signature in composer +- \#1665 App grid: Manage 3 apps and less beautifully +- \#1544 \[UX\] User can not move to another fields by press TAB +- \#1663 \[Compose\] Use cannot click to compose after system display an error message +- \#1654 \[SLO\] Redirect to SLO page of the OIDC provider +- \#1631 \[Notification\] User cannot receive new email notification +- \#1666 \[COMPOSER\] Keyboard overriding rich text context menu on mobile +- \#1569 \[BackgroundApp\] Android system keeps showing an app that run at background +- \#1749 \[Drats\] User cannot save drafts or send email if that email already at drafts mailbox + +## [0.7.6] - 2023-04-06 +### Added +- \#1510 right click for app grid item +- Handle text, contact in Share with TeamMail in Android +- Translation +- \#1581 Support RTL +- \#1606 support relative path in Session + +### Changed +- \#1487 upgrade to Flutter 3.7.5 +- Update the error handler with BadCredentialException +- Increase minimum supported iOS version to 11 +- Cache settings for nginx +- \#997 new design for date-range-picker + +### Fixed +- Auto scroll when expand mailbox +- \#1472 fix position of Toast in mobile +- \#1513 richtext toolbar is lost in mobile +- \#1477 fix search +- \#1521 fix can not scroll to read long email in Android +- \#1527 fix focus in composer +- \#1162 fix open link +- \#1549 fix overlapped long text +- \#1539 support space for inputing name in auto suggestion in Search +- \#1528 input indicator is cut at the bottom of composer +- \#1594 can not send email because Controller is killed +- Fix drag n drop email +- \#1573 fix cursor in Android +- \#1611 prevent blocking when user input html in vacation +- \#1604 missing capability for team mailbox +- \#1440 user can not sign in with OIDC when press back button in auth page +- \#1657 fix broked infinite scroll + +### Removed +- Remove menu action of Team Mailbox +- \#1508 setBackgroundMessageHandler +- \#1512 remove plain text input for signature +- \#1469 remove animation when navigating screen + +[0.9.3]: https://github.com/linagora/tmail-flutter/releases/tag/v0.9.3 +[0.9.2]: https://github.com/linagora/tmail-flutter/releases/tag/v0.9.2 +[0.9.1]: https://github.com/linagora/tmail-flutter/releases/tag/v0.9.1 +[0.9.0]: https://github.com/linagora/tmail-flutter/releases/tag/v0.9.0 +[0.8.9]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.9 +[0.8.8]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.8 +[0.8.7]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.7 +[0.8.6]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.6 +[0.8.5]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.5 +[0.8.4]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.4 +[0.8.3]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.3 +[0.8.2]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.2 +[0.8.1]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.1 +[0.8.0]: https://github.com/linagora/tmail-flutter/releases/tag/v0.8.0 +[0.7.11]: https://github.com/linagora/tmail-flutter/releases/tag/v0.7.11 +[0.7.10]: https://github.com/linagora/tmail-flutter/releases/tag/v0.7.10 +[0.7.9]: https://github.com/linagora/tmail-flutter/releases/tag/v0.7.9 +[0.7.8]: https://github.com/linagora/tmail-flutter/releases/tag/v0.7.8 +[0.7.7]: https://github.com/linagora/tmail-flutter/releases/tag/v0.7.7 +[0.7.6]: https://github.com/linagora/tmail-flutter/releases/tag/v0.7.6 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index a4dda0d570..def87bf750 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,6 @@ -#Stage 1 - Install dependencies and build the app -FROM debian:latest AS build-env - -ENV FLUTTER_CHANNEL="stable" -ENV FLUTTER_VERSION="3.0.1" -ENV FLUTTER_URL="https://storage.googleapis.com/flutter_infra_release/releases/$FLUTTER_CHANNEL/linux/flutter_linux_$FLUTTER_VERSION-$FLUTTER_CHANNEL.tar.xz" -ENV FLUTTER_HOME="/opt/flutter" - -ENV PATH "$PATH:$FLUTTER_HOME/bin" - -# Prerequisites -RUN apt update && apt install -y curl git unzip xz-utils zip gzip libglu1-mesa \ - && mkdir -p $FLUTTER_HOME \ - && curl -o flutter.tar.xz $FLUTTER_URL \ - && tar xf flutter.tar.xz -C /opt \ - && rm flutter.tar.xz \ - && flutter doctor \ - && rm -rf /var/lib/{apt,dpkg,cache,log} +# Stage 1 - Install dependencies and build the app +# This matches the flutter version on our CI/CD pipeline on Github +FROM ghcr.io/cirruslabs/flutter:3.10.6 AS build-env # Set directory to Copy App WORKDIR /app @@ -23,7 +8,7 @@ WORKDIR /app COPY . . # Precompile tmail flutter -RUN bash prebuild.sh && flutter build web --profile +RUN bash prebuild.sh && flutter build web --release # Stage 2 - Create the run-time image FROM nginx:mainline @@ -35,4 +20,4 @@ COPY --from=build-env /app/build/web /usr/share/nginx/html EXPOSE 80 # Before stating NGinx, re-zip all the content to ensure customizations are propagated -CMD gzip -k -r /usr/share/nginx/html/ && nginx -g 'daemon off;' +CMD gzip -k -r -f /usr/share/nginx/html/ && nginx -g 'daemon off;' diff --git a/Jenkinsfile b/Jenkinsfile index 3a4008782b..072429c84a 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,7 @@ pipeline { - agent any + agent { + label 'jdk11' + } options { // Configure an overall timeout for the build. diff --git a/LICENSE b/LICENSE index d8a0912416..be3f7b28e5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,668 +1,661 @@ -GNU Affero General Public License version 3 – OpenPaaS - -License and Additional Terms for OpenPaaS software - -OpenPaaS is an open-source, cloud-based and enterprise-centric PaaS and SaaS -dedicated to social and collaborative services, distributed under the GNU -Affero GPL v3 License terms, with Additional Terms pursuant to Section 7 of -said license. - -These Additional Terms are not intended to be taken as a change of heart by -Linagora over the principles of free software and open source distribution, as -Linagora strongly believes in free software and open source distribution, since -it warrants an easy and reasonable access to software innovation to large user -communities, and is highly committed to supporting free software and open -source whenever possible. - -Linagora wishes its paternity over OpenPaaS to be acknowledged, -regardless of its present or later use, modification, distribution and/or -evolutions. Accordingly, these terms aim at preserving Linagora moral rights -over OpenPaaS. - -We have taken care of not affecting product copying, improvements or deploying. -It is our conviction that the community will not be affected by these terms, -the ultimate goal of which is to ensure the sustainability of free and open -source software by supporting R&D and improving the visibility of Linagora as a -free and open source software publisher, while encouraging others to comply -with our common ideals. - -Pursuant to this license, you are therefore free to use the software and modify -it according to the GNU Affero General Public License version 3, provided that -you comply with its requirements, notably: - - - indicating, in a clear and unambiguous manner, that the software is a - modification of original code; - - retaining Appropriate Legal Notices in the source code and the user - interface; - - keeping any modifications of the software under the terms of the GNU Affero - General Public License version 3, including its Additional Terms pursuant to - its section 7, subsections (b), (c) and (e). - -Following are the applicable Additional Terms for use of OpenPaaS -pursuant to section 7, subsections (b), (c) and (e) of the GNU Affero General -Public License version 3. - -Additional Terms applicable for OpenPaaS - -The following additional terms are applicable to the use, modification and -distribution of OpenPaaS: - - 1. Notices and Attribution: - -The interactive user interfaces in modified source and object code versions of -this program must display Appropriate Legal Notices, as required under Section -5 of the GNU Affero General Public License version 3. - -In accordance with Section 7 and subsection (b) of the GNU Affero General -Public License version 3, these Appropriate Legal Notices consist in the -display of the Signature Notice “OpenPaaS is powered by Linagora.” for any -and all type of outbound messages (e.g. e-mail and meeting requests). -Retaining this Signature Notice in any and all free and Open Source versions -of OpenPaaS is mandatory notwhistanding any other terms and conditions. - -These Signature Notices can be freely translated and replaced by any notice of -strictly identical meaning in another language according to localization of the -software, provided such notice clearly displays the words “OpenPaaS” and -“Linagora”. - -Regardless of the notice language, the Logo/words "OpenPaaS" must be a clickable -hypertext link that leads directly to the Internet URL http://open-paas.org. -The Logo/word "Linagora" must be a clickable hypertext link that leads directly -to the Internet URL http://www.linagora.com. - -2. Use of the OpenPaaS and Linagora trademarks and logos - -OpenPaaS™ and Linagora™ are registered trademarks of Linagora. - -Pursuant to Section 7, subsections (c) and (e) of the GNU Affero General Public -License version 3, this license allows limited use of these trademarks under -the following terms: - -All Linagora trademarks, including OpenPaaS™ and Linagora™ logos shall be used by -the licensees and sublicensees for the sole purpose of complying with the -present Additional Terms to the GNU Affero General Public License version 3, -excluding any other purpose without written consent obtained from Linagora. - -Using these trademarks without the (TM) trademark notice symbol, removing these -trademarks from the software, modifying these trademarks in any manner except -proportional scaling (under the proviso that such scaling keeps the trademark -clearly legible), or using these trademarks to promote any products or services -commercially, or on product packaging, websites, books, documentation or any -other publication without a written, signed agreement with Linagora is strictly -prohibited, and constitutes an infringement of Linagora intellectual property -rights over these trademarks. Using these trademarks in a way harmful, -damaging or detrimental to the value of the OpenPaaS brand or any other Linagora -trademarks, integrity, image, reputation, and/or goodwill, as determined by -Linagora, is also strictly prohibited, and constitutes an infringement of -Linagora intellectual property rights over these trademarks as well. - -Please report any possible violation of the GNU Affero General Public License -version 3, any violation of the hereabove Additional Terms, any infringement -and/or misuse of any OpenPaaS or Linagora trade marks and/or a violation of the -aforementioned Trademark Policy at . - - GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November - 2007 - -Copyright © 2007 Free Software Foundation, Inc. Everyone is -permitted to copy and distribute verbatim copies of this license document, but -changing it is not allowed. - - Preamble - -The GNU Affero General Public License is a free, copyleft license for software -and other kinds of works, specifically designed to ensure cooperation with the -community in the case of network server software. - -The licenses for most software and other practical works are designed to take -away your freedom to share and change the works. By contrast, our General -Public Licenses are intended to guarantee your freedom to share and change all -versions of a program--to make sure it remains free software for all its users. - -When we speak of free software, we are referring to freedom, not price. Our -General Public Licenses are designed to make sure that you have the freedom to -distribute copies of free software (and charge for them if you wish), that you -receive source code or can get it if you want it, that you can change the -software or use pieces of it in new free programs, and that you know you can do -these things. - -Developers that use our General Public Licenses protect your rights with two -steps: (1) assert copyright on the software, and (2) offer you this License -which gives you legal permission to copy, distribute and/or modify the -software. - -A secondary benefit of defending all users' freedom is that improvements made -in alternate versions of the program, if they receive widespread use, become -available for other developers to incorporate. Many developers of free software -are heartened and encouraged by the resulting cooperation. However, in the case -of software used on network servers, this result may fail to come about. The -GNU General Public License permits making a modified version and letting the -public access it on a server without ever releasing its source code to the -public. - -The GNU Affero General Public License is designed specifically to ensure that, -in such cases, the modified source code becomes available to the community. It -requires the operator of a network server to provide the source code of the -modified version running there to the users of that server. Therefore, public -use of a modified version, on a publicly accessible server, gives the public -access to the source code of the modified version. - -An older license, called the Affero General Public License and published by -Affero, was designed to accomplish similar goals. This is a different license, -not a version of the Affero GPL, but Affero has released a new version of the -Affero GPL which permits relicensing under this license. - -The precise terms and conditions for copying, distribution and modification -follow. - - TERMS AND CONDITIONS + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS 0. Definitions. -"This License" refers to version 3 of the GNU Affero General Public License. + "This License" refers to version 3 of the GNU Affero General Public License. -"Copyright" also means copyright-like laws that apply to other kinds of works, -such as semiconductor masks. + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. -"The Program" refers to any copyrightable work licensed under this License. -Each licensee is addressed as "you". "Licensees" and "recipients" may be -individuals or organizations. + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. -To "modify" a work means to copy from or adapt all or part of the work in a -fashion requiring copyright permission, other than the making of an exact copy. -The resulting work is called a "modified version" of the earlier work or a work -"based on" the earlier work. + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. -A "covered work" means either the unmodified Program or a work based on the -Program. + A "covered work" means either the unmodified Program or a work based +on the Program. -To "propagate" a work means to do anything with it that, without permission, -would make you directly or secondarily liable for infringement under applicable -copyright law, except executing it on a computer or modifying a private copy. -Propagation includes copying, distribution (with or without modification), -making available to the public, and in some countries other activities as well. + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. -To "convey" a work means any kind of propagation that enables other parties to -make or receive copies. Mere interaction with a user through a computer -network, with no transfer of a copy, is not conveying. + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. -An interactive user interface displays "Appropriate Legal Notices" to the -extent that it includes a convenient and prominently visible feature that (1) -displays an appropriate copyright notice, and (2) tells the user that there is -no warranty for the work (except to the extent that warranties are provided), -that licensees may convey the work under this License, and how to view a copy -of this License. If the interface presents a list of user commands or options, -such as a menu, a prominent item in the list meets this criterion. + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. 1. Source Code. -The "source code" for a work means the preferred form of the work for making -modifications to it. "Object code" means any non-source form of a work. A -"Standard Interface" means an interface that either is an official standard -defined by a recognized standards body, or, in the case of interfaces specified -for a particular programming language, one that is widely used among developers -working in that language. - -The "System Libraries" of an executable work include anything, other than the -work as a whole, that (a) is included in the normal form of packaging a Major -Component, but which is not part of that Major Component, and (b) serves only -to enable use of the work with that Major Component, or to implement a Standard -Interface for which an implementation is available to the public in source code -form. A "Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system (if any) on -which the executable work runs, or a compiler used to produce the work, or an -object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all the source -code needed to generate, install, and (for an executable work) run the object -code and to modify the work, including scripts to control those activities. -However, it does not include the work's System Libraries, or general-purpose -tools or generally available free programs which are used unmodified in -performing those activities but which are not part of the work. For example, -Corresponding Source includes interface definition files associated with source -files for the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, such as -by intimate data communication or control flow between those subprograms and -other parts of the work. - -The Corresponding Source need not include anything that users can regenerate -automatically from other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same work. + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. 2. Basic Permissions. -All rights granted under this License are granted for the term of copyright on -the Program, and are irrevocable provided the stated conditions are met. This -License explicitly affirms your unlimited permission to run the unmodified -Program. The output from running a covered work is covered by this License only -if the output, given its content, constitutes a covered work. This License -acknowledges your rights of fair use or other equivalent, as provided by -copyright law. - -You may make, run and propagate covered works that you do not convey, without -conditions so long as your license otherwise remains in force. You may convey -covered works to others for the sole purpose of having them make modifications -exclusively for you, or provide you with facilities for running those works, -provided that you comply with the terms of this License in conveying all -material for which you do not control copyright. Those thus making or running -the covered works for you must do so exclusively on your behalf, under your -direction and control, on terms that prohibit them from making any copies of + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. -Conveying under any other circumstances is permitted solely under the -conditions stated below. Sublicensing is not allowed; section 10 makes it -unnecessary. + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. -No covered work shall be deemed part of an effective technological measure -under any applicable law fulfilling obligations under article 11 of the WIPO -copyright treaty adopted on 20 December 1996, or similar laws prohibiting or -restricting circumvention of such measures. - -When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention is -effected by exercising rights under this License with respect to the covered -work, and you disclaim any intention to limit operation or modification of the -work as a means of enforcing, against the work's users, your or third parties' -legal rights to forbid circumvention of technological measures. 4. Conveying -Verbatim Copies. - -You may convey verbatim copies of the Program's source code as you receive it, -in any medium, provided that you conspicuously and appropriately publish on -each copy an appropriate copyright notice; keep intact all notices stating that -this License and any non-permissive terms added in accord with section 7 apply -to the code; keep intact all notices of the absence of any warranty; and give -all recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, and you may -offer support or warranty protection for a fee. + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. -You may convey a work based on the Program, or the modifications to produce it -from the Program, in the form of source code under the terms of section 4, -provided that you also meet all of these conditions: + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: - a) The work must carry prominent notices stating that you modified it, and -giving a relevant date. + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. - b) The work must carry prominent notices stating that it is released under -this License and any conditions added under section 7. This requirement -modifies the requirement in section 4 to "keep intact all notices". + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". - c) You must license the entire work, as a whole, under this License to -anyone who comes into possession of a copy. This License will therefore apply, -along with any applicable section 7 additional terms, to the whole of the work, -and all its parts, regardless of how they are packaged. This License gives no -permission to license the work in any other way, but it does not invalidate -such permission if you have separately received it. + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display -Appropriate Legal Notices; however, if the Program has interactive interfaces -that do not display Appropriate Legal Notices, your work need not make them do -so. - -A compilation of a covered work with other separate and independent works, -which are not by their nature extensions of the covered work, and which are not -combined with it such as to form a larger program, in or on a volume of a -storage or distribution medium, is called an "aggregate" if the compilation and -its resulting copyright are not used to limit the access or legal rights of the -compilation's users beyond what the individual works permit. Inclusion of a -covered work in an aggregate does not cause this License to apply to the other + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. -You may convey a covered work in object code form under the terms of sections 4 -and 5, provided that you also convey the machine-readable Corresponding Source -under the terms of this License, in one of these ways: - -a) Convey the object code in, or embodied in, a physical product (including a -physical distribution medium), accompanied by the Corresponding Source fixed on -a durable physical medium customarily used for software interchange. - -b) Convey the object code in, or embodied in, a physical product (including a -physical distribution medium), accompanied by a written offer, valid for at -least three years and valid for as long as you offer spare parts or customer -support for that product model, to give anyone who possesses the object code -either (1) a copy of the Corresponding Source for all the software in the -product that is covered by this License, on a durable physical medium -customarily used for software interchange, for a price no more than your -reasonable cost of physically performing this conveying of source, or (2) -access to copy the Corresponding Source from a network server at no charge. - -c) Convey individual copies of the object code with a copy of the written offer -to provide the Corresponding Source. This alternative is allowed only -occasionally and noncommercially, and only if you received the object code with -such an offer, in accord with subsection 6b. - -d) Convey the object code by offering access from a designated place (gratis or -for a charge), and offer equivalent access to the Corresponding Source in the -same way through the same place at no further charge. You need not require -recipients to copy the Corresponding Source along with the object code. If the -place to copy the object code is a network server, the Corresponding Source may -be on a different server (operated by you or a third party) that supports -equivalent copying facilities, provided you maintain clear directions next to -the object code saying where to find the Corresponding Source. Regardless of -what server hosts the Corresponding Source, you remain obligated to ensure that -it is available for as long as needed to satisfy these requirements. - -e) Convey the object code using peer-to-peer transmission, provided you inform -other peers where the object code and Corresponding Source of the work are -being offered to the general public at no charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded from the -Corresponding Source as a System Library, need not be included in conveying the -object code work. - -A "User Product" is either (1) a "consumer product", which means any tangible -personal property which is normally used for personal, family, or household -purposes, or (2) anything designed or sold for incorporation into a dwelling. -In determining whether a product is a consumer product, doubtful cases shall be -resolved in favor of coverage. For a particular product received by a -particular user, "normally used" refers to a typical or common use of that -class of product, regardless of the status of the particular user or of the way -in which the particular user actually uses, or expects or is expected to use, -the product. A product is a consumer product regardless of whether the product -has substantial commercial, industrial or non-consumer uses, unless such uses -represent the only significant mode of use of the product. - -"Installation Information" for a User Product means any methods, procedures, -authorization keys, or other information required to install and execute -modified versions of a covered work in that User Product from a modified -version of its Corresponding Source. The information must suffice to ensure -that the continued functioning of the modified object code is in no case -prevented or interfered with solely because modification has been made. If you -convey an object code work under this section in, or with, or specifically for -use in, a User Product, and the conveying occurs as part of a transaction in -which the right of possession and use of the User Product is transferred to the -recipient in perpetuity or for a fixed term (regardless of how the transaction -is characterized), the Corresponding Source conveyed under this section must be -accompanied by the Installation Information. But this requirement does not -apply if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has been -installed in ROM). - -The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates for a -work that has been modified or installed by the recipient, or for the User -Product in which it has been modified or installed. Access to a network may be -denied when the modification itself materially and adversely affects the -operation of the network or violates the rules and protocols for communication -across the network. - -Corresponding Source conveyed, and Installation Information provided, in accord -with this section must be in a format that is publicly documented (and with an -implementation available to the public in source code form), and must require -no special password or key for unpacking, reading or copying. + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. 7. Additional Terms. -"Additional permissions" are terms that supplement the terms of this License by -making exceptions from one or more of its conditions. Additional permissions -that are applicable to the entire Program shall be treated as though they were -included in this License, to the extent that they are valid under applicable -law. If additional permissions apply only to part of the Program, that part may -be used separately under those permissions, but the entire Program remains -governed by this License without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option remove any -additional permissions from that copy, or from any part of it. (Additional -permissions may be written to require their own removal in certain cases when -you modify the work.) You may place additional permissions on material, added -by you to a covered work, for which you have or can give appropriate copyright -permission. - -Notwithstanding any other provision of this License, for material you add to a -covered work, you may (if authorized by the copyright holders of that material) -supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the terms of -sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or author -attributions in that material or in the Appropriate Legal Notices displayed by -works containing it; or + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or -requiring that modified versions of such material be marked in reasonable ways -as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or authors -of the material; or - - e) Declining to grant rights under trademark law for use of some trade -names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that material by -anyone who conveys the material (or modified versions of it) with contractual -assumptions of liability to the recipient, for any liability that these -contractual assumptions directly impose on those licensors and authors. - -All other non-permissive additional terms are considered "further restrictions" -within the meaning of section 10. If the Program as you received it, or any -part of it, contains a notice stating that it is governed by this License along -with a term that is a further restriction, you may remove that term. If a -license document contains a further restriction but permits relicensing or -conveying under this License, you may add to a covered work material governed -by the terms of that license document, provided that the further restriction -does not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you must place, -in the relevant source files, a statement of the additional terms that apply to -those files, or a notice indicating where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the form of a -separately written license, or stated as exceptions; the above requirements -apply either way. + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. 8. Termination. -You may not propagate or modify a covered work except as expressly provided -under this License. Any attempt otherwise to propagate or modify it is void, -and will automatically terminate your rights under this License (including any -patent licenses granted under the third paragraph of section 11). However, if -you cease all violation of this License, then your license from a particular -copyright holder is reinstated (a) provisionally, unless and until the -copyright holder explicitly and finally terminates your license, and (b) -permanently, if the copyright holder fails to notify you of the violation by -some reasonable means prior to 60 days after the cessation. - -Moreover, your license from a particular copyright holder is reinstated -permanently if the copyright holder notifies you of the violation by some -reasonable means, this is the first time you have received notice of violation -of this License (for any work) from that copyright holder, and you cure the -violation prior to 30 days after your receipt of the notice. - -Termination of your rights under this section does not terminate the licenses -of parties who have received copies or rights from you under this License. If -your rights have been terminated and not permanently reinstated, you do not -qualify to receive new licenses for the same material under section 10. + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. 9. Acceptance Not Required for Having Copies. -You are not required to accept this License in order to receive or run a copy -of the Program. Ancillary propagation of a covered work occurring solely as a -consequence of using peer-to-peer transmission to receive a copy likewise does -not require acceptance. However, nothing other than this License grants you -permission to propagate or modify any covered work. These actions infringe -copyright if you do not accept this License. Therefore, by modifying or -propagating a covered work, you indicate your acceptance of this License to do -so. + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. -Each time you convey a covered work, the recipient automatically receives a -license from the original licensors, to run, modify and propagate that work, -subject to this License. You are not responsible for enforcing compliance by -third parties with this License. + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. -An "entity transaction" is a transaction transferring control of an + An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered work -results from an entity transaction, each party to that transaction who receives -a copy of the work also receives whatever licenses to the work the party's -predecessor in interest had or could give under the previous paragraph, plus a -right to possession of the Corresponding Source of the work from the -predecessor in interest, if the predecessor has it or can get it with -reasonable efforts. - -You may not impose any further restrictions on the exercise of the rights -granted or affirmed under this License. For example, you may not impose a -license fee, royalty, or other charge for exercise of rights granted under this -License, and you may not initiate litigation (including a cross-claim or -counterclaim in a lawsuit) alleging that any patent claim is infringed by -making, using, selling, offering for sale, or importing the Program or any -portion of it. +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. 11. Patents. -A "contributor" is a copyright holder who authorizes use under this License of -the Program or a work on which the Program is based. The work thus licensed is -called the contributor's "contributor version". - -A contributor's "essential patent claims" are all patent claims owned or -controlled by the contributor, whether already acquired or hereafter acquired, -that would be infringed by some manner, permitted by this License, of making, -using, or selling its contributor version, but do not include claims that would -be infringed only as a consequence of further modification of the contributor -version. For purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of this -License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free patent -license under the contributor's essential patent claims, to make, use, sell, -offer for sale, import and otherwise run, modify and propagate the contents of -its contributor version. - -In the following three paragraphs, a "patent license" is any express agreement -or commitment, however denominated, not to enforce a patent (such as an express -permission to practice a patent or covenant not to sue for patent -infringement). To "grant" such a patent license to a party means to make such -an agreement or commitment not to enforce a patent against the party. - -If you convey a covered work, knowingly relying on a patent license, and the -Corresponding Source of the work is not available for anyone to copy, free of -charge and under the terms of this License, through a publicly available -network server or other readily accessible means, then you must either (1) -cause the Corresponding Source to be so available, or (2) arrange to deprive -yourself of the benefit of the patent license for this particular work, or (3) -arrange, in a manner consistent with the requirements of this License, to -extend the patent license to downstream recipients. "Knowingly relying" means -you have actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work in a -country, would infringe one or more identifiable patents in that country that -you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or arrangement, you -convey, or propagate by procuring conveyance of, a covered work, and grant a -patent license to some of the parties receiving the covered work authorizing -them to use, propagate, modify or convey a specific copy of the covered work, -then the patent license you grant is automatically extended to all recipients -of the covered work and works based on it. - -A patent license is "discriminatory" if it does not include within the scope of -its coverage, prohibits the exercise of, or is conditioned on the non-exercise -of one or more of the rights that are specifically granted under this License. -You may not convey a covered work if you are a party to an arrangement with a -third party that is in the business of distributing software, under which you -make payment to the third party based on the extent of your activity of -conveying the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory patent -license (a) in connection with copies of the covered work conveyed by you (or -copies made from those copies), or (b) primarily for and in connection with -specific products or compilations that contain the covered work, unless you -entered into that arrangement, or that patent license was granted, prior to 28 -March 2007. - -Nothing in this License shall be construed as excluding or limiting any implied -license or other defenses to infringement that may otherwise be available to -you under applicable patent law. + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. -If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not excuse -you from the conditions of this License. If you cannot convey a covered work so -as to satisfy simultaneously your obligations under this License and any other -pertinent obligations, then as a consequence you may not convey it at all. For -example, if you agree to terms that obligate you to collect a royalty for -further conveying from those to whom you convey the Program, the only way you -could satisfy both those terms and this License would be to refrain entirely -from conveying the Program. + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. -Notwithstanding any other provision of this License, if you modify the Program, -your modified version must prominently offer all users interacting with it -remotely through a computer network (if your version supports such interaction) -an opportunity to receive the Corresponding Source of your version by providing -access to the Corresponding Source from a network server at no charge, through -some standard or customary means of facilitating copying of software. This -Corresponding Source shall include the Corresponding Source for any work -covered by version 3 of the GNU General Public License that is incorporated -pursuant to the following paragraph. - -Notwithstanding any other provision of this License, you have permission to -link or combine any covered work with a work licensed under version 3 of the -GNU General Public License into a single combined work, and to convey the -resulting work. The terms of this License will continue to apply to the part -which is the covered work, but the work with which it is combined will remain -governed by version 3 of the GNU General Public License. 14. Revised Versions -of this License. - -The Free Software Foundation may publish revised and/or new versions of the GNU -Affero General Public License from time to time. Such new versions will be -similar in spirit to the present version, but may differ in detail to address -new problems or concerns. - -Each version is given a distinguishing version number. If the Program specifies -that a certain numbered version of the GNU Affero General Public License "or -any later version" applies to it, you have the option of following the terms -and conditions either of that numbered version or of any later version -published by the Free Software Foundation. If the Program does not specify a -version number of the GNU Affero General Public License, you may choose any -version ever published by the Free Software Foundation. - -If the Program specifies that a proxy can decide which future versions of the -GNU Affero General Public License can be used, that proxy's public statement of -acceptance of a version permanently authorizes you to choose that version for -the Program. - -Later license versions may give you additional or different permissions. -However, no additional obligations are imposed on any author or copyright -holder as a result of your choosing to follow a later version. + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. 15. Disclaimer of Warranty. -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE -LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER -PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER -EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE -QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE -DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR -CORRECTION. + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY -COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS -PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, -INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE -THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED -INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE -PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY -HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. -If the disclaimer of warranty and limitation of liability provided above cannot -be given local legal effect according to their terms, reviewing courts shall -apply local law that most closely approximates an absolute waiver of all civil -liability in connection with the Program, unless a warranty or assumption of -liability accompanies a copy of the Program in return for a fee. + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. -END OF TERMS AND CONDITIONS + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/README.md b/README.md index 8f1bc74082..8d034080f3 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,19 @@ # Team Mail Flutter mobile application [![Gitter](https://badges.gitter.im/linagora/team-mail.svg)](https://gitter.im/linagora/team-mail?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +[![Documentation](https://img.shields.io/badge/Documentation-green.svg)](docs) +[![Images docker](https://img.shields.io/badge/Images-docker-blue.svg)](https://hub.docker.com/r/linagora/tmail-web) +[![Android application](https://img.shields.io/badge/App-Android-blue.svg)](https://play.google.com/store/apps/details?id=com.linagora.android.teammail) +[![Ios application](https://img.shields.io/badge/App-Ios-blue.svg)](https://apps.apple.com/gr/app/teammail/id1587086189) ![LOGO](https://user-images.githubusercontent.com/6462404/202656316-8b77a7b6-0c1f-4f3e-932b-72bd446b6605.png) + This project aims at providing a multi-platform mobile email application, running the [JMAP protocol](https://jmap.io/) and will also deliver additional features to the [Team Mail back-end](https://github.com/linagora/tmail-backend). +Team-mail is developed with love by [Linagora](https://linagora.com). + Here is how Team Mail looks like on a phone: ![Screenshots Mobile](https://user-images.githubusercontent.com/6462404/169979675-85893fa4-325a-426b-a1a8-0751a585954a.png) @@ -93,16 +100,30 @@ This versatility is enabled by the use of the [Flutter framework](https://flutte
Read more... -First, we plan to write a simple, multi-platform JMAP email client. This includes reading your mails and mailboxes, managing them, sending emails, searching your emails. This will likely keep us busy by the end of 2021. +Now that we plan having a simple JMAP email client supporting Android, IOS, and a webmail, we are working on some extra features on top of the TeamMail backend, including: + + - Better filters, with more actions, and combining conditions + - Restoring deleted messages + - Delegating full access to others for instance your secratery + - Labels for better sorting your emails accross folders + - Automated actions: archiving, emptying your trash, your spam folders + - Running filters against a folder + - Attachment thumbnails + +We are also planning active work on drag and drops and other user experience / productivity enhancements. + +We do not currently plan working on desktop applications, on websockets for push on top of TeamMail web but such contributions would be appreciated. We also +welcome feedback and pull requests regarding Team-Mail portability (running TeamMail on top of third party mail servers). + +First, we plan to write a simple, multi-platform JMAP email client. This includes reading your mails and mailboxes, managing them, sending emails, searching your emails. +This will likely keep us busy by the end of 2021. Then, we have plan for multiple features including: - Support for Team Mail encrypted mailbox (GPG) - - Support for Team Mail shared mailboxes - - Support for Team Mail filters - Interactions with some other software from [Linagora](https://linagora.com) including: - - Sending attachments via [LinShare]() file sharing platform. - - Transfering some attachments you received to [LinShare](https://www.linshare.org/fr/accueil/) file sharing platform. + - Sending attachments via [TDrive](https://github.com/linagora/TDrive) file sharing platform. + - Transfering some attachments you received to [TDrive](https://github.com/linagora/TDrive) file sharing platform. - Discussing some emails you received via [Twake](https://twake.app/en/) chat.
diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4de17..195df97751 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -10,20 +10,7 @@ include: package:flutter_lints/flutter.yaml linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + constant_identifier_names: false + non_constant_identifier_names: false + unnecessary_string_escapes: false diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9eaa465613..5639ceb1c0 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -11,6 +11,8 @@ + + @@ -43,8 +45,6 @@ - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/values-night-v29/styles.xml b/android/app/src/main/res/values-night-v29/styles.xml deleted file mode 100644 index aa87be1aa1..0000000000 --- a/android/app/src/main/res/values-night-v29/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - \ No newline at end of file diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml deleted file mode 100644 index a9464b2978..0000000000 --- a/android/app/src/main/res/values-night-v31/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml index 7c806c658b..b07271df34 100644 --- a/android/app/src/main/res/values-night/styles.xml +++ b/android/app/src/main/res/values-night/styles.xml @@ -1,11 +1,15 @@ - + \ No newline at end of file diff --git a/android/app/src/main/res/values-v21/styles.xml b/android/app/src/main/res/values-v21/styles.xml deleted file mode 100644 index 9cce85e65b..0000000000 --- a/android/app/src/main/res/values-v21/styles.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/values-v29/styles.xml b/android/app/src/main/res/values-v29/styles.xml deleted file mode 100644 index b0859f4ecd..0000000000 --- a/android/app/src/main/res/values-v29/styles.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml deleted file mode 100644 index b050743abe..0000000000 --- a/android/app/src/main/res/values-v31/styles.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index c8a5e60948..c170e662c6 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -1,11 +1,15 @@ - + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 6ce9414cce..86463c2f28 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,20 +2,20 @@ buildscript { ext.kotlin_version = '1.6.10' repositories { google() - jcenter() + mavenCentral() } dependencies { classpath 'com.google.gms:google-services:4.3.10' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath 'com.android.tools.build:gradle:7.0.2' + classpath 'com.android.tools.build:gradle:7.1.3' } } allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -25,6 +25,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 81212ccfd9..187896baee 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -20,7 +20,7 @@ default_platform(:android) platform :android do desc "Build development version" lane :dev do - sh "flutter build apk --verbose --release --dart-define=SERVER_URL=$SERVER_URL" + sh "flutter build apk --verbose --debug --dart-define=SERVER_URL=$SERVER_URL" end desc "Build and deploy release version" diff --git a/android/fastlane/README.md b/android/fastlane/README.md new file mode 100644 index 0000000000..5b34d57511 --- /dev/null +++ b/android/fastlane/README.md @@ -0,0 +1,40 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## Android + +### android dev + +```sh +[bundle exec] fastlane android dev +``` + +Build development version + +### android release + +```sh +[bundle exec] fastlane android release +``` + +Build and deploy release version + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index b8793d3c0d..cc5527d781 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip diff --git a/assets/icons/icon_logo.png b/assets/icons/icon_logo.png new file mode 100644 index 0000000000..c347a070ca Binary files /dev/null and b/assets/icons/icon_logo.png differ diff --git a/assets/images/2.0x/login_graphic.png b/assets/images/2.0x/login_graphic.png deleted file mode 100644 index 1d5fa72c63..0000000000 Binary files a/assets/images/2.0x/login_graphic.png and /dev/null differ diff --git a/assets/images/2.0x/logo_tmail.png b/assets/images/2.0x/logo_tmail.png deleted file mode 100644 index c4b6ac0455..0000000000 Binary files a/assets/images/2.0x/logo_tmail.png and /dev/null differ diff --git a/assets/images/3.0x/login_graphic.png b/assets/images/3.0x/login_graphic.png deleted file mode 100644 index 48fa5849cc..0000000000 Binary files a/assets/images/3.0x/login_graphic.png and /dev/null differ diff --git a/assets/images/3.0x/logo_tmail.png b/assets/images/3.0x/logo_tmail.png deleted file mode 100644 index 7baa274a31..0000000000 Binary files a/assets/images/3.0x/logo_tmail.png and /dev/null differ diff --git a/assets/images/ic_add_picture.svg b/assets/images/ic_add_picture.svg new file mode 100644 index 0000000000..9db70fafc6 --- /dev/null +++ b/assets/images/ic_add_picture.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/images/ic_arrow_bottom.svg b/assets/images/ic_arrow_bottom.svg new file mode 100644 index 0000000000..20bc50dc40 --- /dev/null +++ b/assets/images/ic_arrow_bottom.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_arrow_left.svg b/assets/images/ic_arrow_left.svg new file mode 100644 index 0000000000..2859a8d7c7 --- /dev/null +++ b/assets/images/ic_arrow_left.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_arrow_right.svg b/assets/images/ic_arrow_right.svg new file mode 100644 index 0000000000..3b2c446e44 --- /dev/null +++ b/assets/images/ic_arrow_right.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_attach_file.svg b/assets/images/ic_attach_file.svg new file mode 100644 index 0000000000..4fa9fd3853 --- /dev/null +++ b/assets/images/ic_attach_file.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_attachments_composer.svg b/assets/images/ic_attachments_composer.svg deleted file mode 100644 index 9dacdfabe9..0000000000 --- a/assets/images/ic_attachments_composer.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/images/ic_avatar_group.svg b/assets/images/ic_avatar_group.svg new file mode 100644 index 0000000000..f049ac79eb --- /dev/null +++ b/assets/images/ic_avatar_group.svg @@ -0,0 +1,6 @@ + + + + diff --git a/assets/images/ic_avatar_group_delivering.svg b/assets/images/ic_avatar_group_delivering.svg new file mode 100644 index 0000000000..73d7eb9b5e --- /dev/null +++ b/assets/images/ic_avatar_group_delivering.svg @@ -0,0 +1,6 @@ + + + + diff --git a/assets/images/ic_avatar_personal.svg b/assets/images/ic_avatar_personal.svg new file mode 100644 index 0000000000..2d09f209c9 --- /dev/null +++ b/assets/images/ic_avatar_personal.svg @@ -0,0 +1,6 @@ + + + + diff --git a/assets/images/ic_avatar_personal_delivering.svg b/assets/images/ic_avatar_personal_delivering.svg new file mode 100644 index 0000000000..8edc70a74d --- /dev/null +++ b/assets/images/ic_avatar_personal_delivering.svg @@ -0,0 +1,6 @@ + + + + diff --git a/assets/images/ic_calendar_event.svg b/assets/images/ic_calendar_event.svg new file mode 100644 index 0000000000..be965e4fdb --- /dev/null +++ b/assets/images/ic_calendar_event.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_cancel.svg b/assets/images/ic_cancel.svg new file mode 100644 index 0000000000..f934a56800 --- /dev/null +++ b/assets/images/ic_cancel.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_close_advanced_search.svg b/assets/images/ic_circle_close.svg similarity index 100% rename from assets/images/ic_close_advanced_search.svg rename to assets/images/ic_circle_close.svg diff --git a/assets/images/ic_close.svg b/assets/images/ic_close.svg index cb1b7c3400..ab4826af2b 100644 --- a/assets/images/ic_close.svg +++ b/assets/images/ic_close.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/images/ic_close_composer.svg b/assets/images/ic_close_composer.svg deleted file mode 100644 index ab4826af2b..0000000000 --- a/assets/images/ic_close_composer.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/assets/images/ic_close_mailbox.svg b/assets/images/ic_close_mailbox.svg deleted file mode 100644 index f5c33a4af9..0000000000 --- a/assets/images/ic_close_mailbox.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/images/ic_connected_internet.svg b/assets/images/ic_connected_internet.svg new file mode 100644 index 0000000000..15017ad6c3 --- /dev/null +++ b/assets/images/ic_connected_internet.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_delete_toast.svg b/assets/images/ic_delete_toast.svg index 2c82dd2702..155a9b1a0d 100644 --- a/assets/images/ic_delete_toast.svg +++ b/assets/images/ic_delete_toast.svg @@ -1,4 +1,8 @@ - - + + diff --git a/assets/images/ic_delivering.svg b/assets/images/ic_delivering.svg new file mode 100644 index 0000000000..496e327805 --- /dev/null +++ b/assets/images/ic_delivering.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_dialog_offline_mode.svg b/assets/images/ic_dialog_offline_mode.svg new file mode 100644 index 0000000000..ecdcbe2661 --- /dev/null +++ b/assets/images/ic_dialog_offline_mode.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_drop_zone_icon.svg b/assets/images/ic_drop_zone_icon.svg new file mode 100644 index 0000000000..37ec0f09e1 --- /dev/null +++ b/assets/images/ic_drop_zone_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_error.svg b/assets/images/ic_error.svg new file mode 100644 index 0000000000..503936c0e1 --- /dev/null +++ b/assets/images/ic_error.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_event_canceled.svg b/assets/images/ic_event_canceled.svg new file mode 100644 index 0000000000..7b011b5a95 --- /dev/null +++ b/assets/images/ic_event_canceled.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_event_invited.svg b/assets/images/ic_event_invited.svg new file mode 100644 index 0000000000..4497cc4d99 --- /dev/null +++ b/assets/images/ic_event_invited.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_event_updated.svg b/assets/images/ic_event_updated.svg new file mode 100644 index 0000000000..c339cd6a42 --- /dev/null +++ b/assets/images/ic_event_updated.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_format_quote.svg b/assets/images/ic_format_quote.svg new file mode 100644 index 0000000000..6806d928a4 --- /dev/null +++ b/assets/images/ic_format_quote.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_forwarded.svg b/assets/images/ic_forwarded.svg new file mode 100644 index 0000000000..02d03edc38 --- /dev/null +++ b/assets/images/ic_forwarded.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_fullscreen.svg b/assets/images/ic_fullscreen.svg new file mode 100644 index 0000000000..005ca75eef --- /dev/null +++ b/assets/images/ic_fullscreen.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_fullscreen_composer.svg b/assets/images/ic_fullscreen_composer.svg deleted file mode 100644 index 9f92b83d76..0000000000 --- a/assets/images/ic_fullscreen_composer.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/assets/images/ic_insert_image.svg b/assets/images/ic_insert_image.svg index 07da9c41b2..f0a764cc83 100644 --- a/assets/images/ic_insert_image.svg +++ b/assets/images/ic_insert_image.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/images/ic_login_graphic.svg b/assets/images/ic_login_graphic.svg new file mode 100644 index 0000000000..71f4c7a474 --- /dev/null +++ b/assets/images/ic_login_graphic.svg @@ -0,0 +1,823 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/ic_mailbox_outbox.svg b/assets/images/ic_mailbox_outbox.svg new file mode 100644 index 0000000000..5c99b1a9ef --- /dev/null +++ b/assets/images/ic_mailbox_outbox.svg @@ -0,0 +1,8 @@ + + + + diff --git a/assets/images/ic_mailbox_sending_queue.svg b/assets/images/ic_mailbox_sending_queue.svg new file mode 100644 index 0000000000..2e0f703d6f --- /dev/null +++ b/assets/images/ic_mailbox_sending_queue.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_mailbox_template.svg b/assets/images/ic_mailbox_template.svg index 98a59c6f9c..772290e3b1 100644 --- a/assets/images/ic_mailbox_template.svg +++ b/assets/images/ic_mailbox_template.svg @@ -1,4 +1,8 @@ - - + + diff --git a/assets/images/ic_menu_mailbox.svg b/assets/images/ic_menu_mailbox.svg new file mode 100644 index 0000000000..72d47d84be --- /dev/null +++ b/assets/images/ic_menu_mailbox.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_minimize.svg b/assets/images/ic_minimize.svg index 999a094a3f..fccb7f8124 100644 --- a/assets/images/ic_minimize.svg +++ b/assets/images/ic_minimize.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/images/ic_read_receipt.svg b/assets/images/ic_read_receipt.svg new file mode 100644 index 0000000000..2549e56529 --- /dev/null +++ b/assets/images/ic_read_receipt.svg @@ -0,0 +1,14 @@ + + + + + + diff --git a/assets/images/ic_read_toast.svg b/assets/images/ic_read_toast.svg index ddd8057d6c..d6b2a0f2a7 100644 --- a/assets/images/ic_read_toast.svg +++ b/assets/images/ic_read_toast.svg @@ -1,4 +1,6 @@ - - + + diff --git a/assets/images/ic_reply_and_forward.svg b/assets/images/ic_reply_and_forward.svg new file mode 100644 index 0000000000..946753f156 --- /dev/null +++ b/assets/images/ic_reply_and_forward.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_rich_toolbar.svg b/assets/images/ic_rich_toolbar.svg new file mode 100644 index 0000000000..fd994920b2 --- /dev/null +++ b/assets/images/ic_rich_toolbar.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/images/ic_save_to_draft.svg b/assets/images/ic_save_to_draft.svg new file mode 100644 index 0000000000..a8611e0e47 --- /dev/null +++ b/assets/images/ic_save_to_draft.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_send.svg b/assets/images/ic_send.svg new file mode 100644 index 0000000000..b28c1fb460 --- /dev/null +++ b/assets/images/ic_send.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/ic_send_disable.svg b/assets/images/ic_send_disable.svg index d17fb28041..d2ce6d5d23 100644 --- a/assets/images/ic_send_disable.svg +++ b/assets/images/ic_send_disable.svg @@ -1,3 +1,3 @@ - + diff --git a/assets/images/ic_send_success_toast.svg b/assets/images/ic_send_success_toast.svg new file mode 100644 index 0000000000..cfac25896a --- /dev/null +++ b/assets/images/ic_send_success_toast.svg @@ -0,0 +1,8 @@ + + + + diff --git a/assets/images/ic_style_code_view.svg b/assets/images/ic_style_code_view.svg index 9c304ebf30..0ccbdd8eae 100644 --- a/assets/images/ic_style_code_view.svg +++ b/assets/images/ic_style_code_view.svg @@ -1,3 +1,3 @@ - - + + diff --git a/assets/images/ic_tmail_logo.svg b/assets/images/ic_tmail_logo.svg new file mode 100644 index 0000000000..07ddce90da --- /dev/null +++ b/assets/images/ic_tmail_logo.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/ic_toast_success_message.svg b/assets/images/ic_toast_success_message.svg new file mode 100644 index 0000000000..e5c947dfa8 --- /dev/null +++ b/assets/images/ic_toast_success_message.svg @@ -0,0 +1,5 @@ + + + diff --git a/assets/images/ic_unread_toast.svg b/assets/images/ic_unread_toast.svg index d2d9a05ad8..f6c8b3c3c1 100644 --- a/assets/images/ic_unread_toast.svg +++ b/assets/images/ic_unread_toast.svg @@ -1,4 +1,8 @@ - - + + diff --git a/assets/images/login_graphic.png b/assets/images/login_graphic.png deleted file mode 100644 index 29a34dadac..0000000000 Binary files a/assets/images/login_graphic.png and /dev/null differ diff --git a/assets/images/logo_tmail.png b/assets/images/logo_tmail.png deleted file mode 100644 index 9b8f1d2329..0000000000 Binary files a/assets/images/logo_tmail.png and /dev/null differ diff --git a/configurations/app_dashboard.json b/configurations/app_dashboard.json index f9be0850fc..57c3178ba2 100644 --- a/configurations/app_dashboard.json +++ b/configurations/app_dashboard.json @@ -8,7 +8,10 @@ { "appName": "LinShare", "icon": "ic_linshare_app.png", - "appLink": "https://linshare.linagora.com/" + "appLink": "https://linshare.linagora.com/", + "androidPackageId": "com.linagora.android.linshare", + "iosUrlScheme": "linshare.mobile", + "iosAppStoreLink": "itms-apps://itunes.apple.com/us/app/linshare/id1534003175" }, { "appName": "Inbox", diff --git a/configurations/icons/ic_calendar_app.svg b/configurations/icons/ic_calendar_app.svg index 357bb94e73..d0d3c6daa9 100644 --- a/configurations/icons/ic_calendar_app.svg +++ b/configurations/icons/ic_calendar_app.svg @@ -1,3 +1,22 @@ -image/svg+xml \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + diff --git a/configurations/icons/ic_contacts_app.svg b/configurations/icons/ic_contacts_app.svg index e7e16e9407..26e7aa68bc 100644 --- a/configurations/icons/ic_contacts_app.svg +++ b/configurations/icons/ic_contacts_app.svg @@ -1,3 +1,40 @@ -image/svg+xml \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/configurations/icons/ic_twake_app.svg b/configurations/icons/ic_twake_app.svg index 0a844bca93..4f34f8ca5d 100644 --- a/configurations/icons/ic_twake_app.svg +++ b/configurations/icons/ic_twake_app.svg @@ -1 +1,24 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contact/analysis_options.yaml b/contact/analysis_options.yaml index a5744c1cfb..0f32754d37 100644 --- a/contact/analysis_options.yaml +++ b/contact/analysis_options.yaml @@ -1,4 +1,16 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +linter: + rules: + constant_identifier_names: false + non_constant_identifier_names: false + unnecessary_string_escapes: false \ No newline at end of file diff --git a/contact/lib/data/datasource/auto_complete_datasource.dart b/contact/lib/data/datasource/auto_complete_datasource.dart index 7d71868cd0..916359a7a5 100644 --- a/contact/lib/data/datasource/auto_complete_datasource.dart +++ b/contact/lib/data/datasource/auto_complete_datasource.dart @@ -1,6 +1,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:model/model.dart'; +import 'package:model/autocomplete/auto_complete_pattern.dart'; abstract class AutoCompleteDataSource { Future> getAutoComplete(AutoCompletePattern autoCompletePattern); diff --git a/contact/pubspec.lock b/contact/pubspec.lock index 99da934c12..5e5916b003 100644 --- a/contact/pubspec.lock +++ b/contact/pubspec.lock @@ -5,142 +5,162 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "61.0.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "5.13.0" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.2.0" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" source: hosted version: "7.2.7" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" + url: "https://pub.dev" source: hosted - version: "8.4.2" + version: "8.4.4" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.5.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.1" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" core: dependency: transitive description: @@ -152,128 +172,122 @@ packages: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" source: hosted version: "0.17.2" cupertino_icons: dependency: transitive description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352" + url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" dartz: dependency: transitive description: name: dartz - url: "https://pub.dartlang.org" + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" source: hosted version: "0.10.1" device_info_plus: dependency: transitive description: name: device_info_plus - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.2" - device_info_plus_linux: - dependency: transitive - description: - name: device_info_plus_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - device_info_plus_macos: - dependency: transitive - description: - name: device_info_plus_macos - url: "https://pub.dartlang.org" + sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" + url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "8.1.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.6.1" - device_info_plus_web: - dependency: transitive - description: - name: device_info_plus_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - device_info_plus_windows: - dependency: transitive - description: - name: device_info_plus_windows - url: "https://pub.dartlang.org" + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "7.0.0" dio: - dependency: transitive + dependency: "direct main" description: name: dio - url: "https://pub.dartlang.org" + sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.0.0" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted version: "6.1.4" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" flex_color_picker: dependency: transitive description: name: flex_color_picker - url: "https://pub.dartlang.org" + sha256: f0e0db8e3e47435cfbe9aa15c71b898fa218be0fc4ae409e1e42d5d5266b2c90 + url: "https://pub.dev" + source: hosted + version: "3.2.0" + flex_seed_scheme: + dependency: transitive + description: + name: flex_seed_scheme + sha256: "7058288ef97d348657ac95cea25d65a9aac181ca08387ede891fd7230ad7600f" + url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "1.2.3" flutter: dependency: "direct main" description: flutter @@ -283,144 +297,202 @@ packages: dependency: transitive description: name: flutter_image_compress - url: "https://pub.dartlang.org" + sha256: "37f1b26399098e5f97b74c1483f534855e7dff68ead6ddaccf747029fb03f29f" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview: + dependency: transitive + description: + name: flutter_inappwebview + sha256: "6d6c741ddba1dba5229d63ba75767064791a7ce845196b45e31105e93d67c949" + url: "https://pub.dev" + source: hosted + version: "6.0.0-beta.22" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "064a8ccbc76217dcd3b0fd6c6ea6f549e69b2849a0233b5bb46af9632c3ce2ff" + url: "https://pub.dev" source: hosted version: "1.1.0" flutter_keyboard_visibility: dependency: transitive description: name: flutter_keyboard_visibility - url: "https://pub.dartlang.org" + sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.4.1" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_keyboard_visibility_platform_interface: dependency: transitive description: name: flutter_keyboard_visibility_platform_interface - url: "https://pub.dartlang.org" + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" source: hosted version: "2.0.0" flutter_keyboard_visibility_web: dependency: transitive description: name: flutter_keyboard_visibility_web - url: "https://pub.dartlang.org" + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" source: hosted version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_svg: dependency: transitive description: name: flutter_svg - url: "https://pub.dartlang.org" + sha256: "97c5b291b4fd34ae4f55d6a4c05841d4d0ed94952e033c5a6529e1b47b4d2a29" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "2.0.2" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_typeahead: + dependency: transitive + description: + name: flutter_typeahead + sha256: f31211a8536f87908c3dcbdb88666e2f4d77f5f06c2b3a48eaad5599969ff32d + url: "https://pub.dev" + source: hosted + version: "4.6.0" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" - fluttertoast: - dependency: transitive - description: - name: fluttertoast - url: "https://pub.dartlang.org" - source: hosted - version: "8.0.8" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.2.0" get: dependency: transitive description: name: get - url: "https://pub.dartlang.org" + sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + url: "https://pub.dev" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" source: hosted version: "2.2.0" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.15.1" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted - version: "0.13.4" + version: "0.13.5" http_mock_adapter: dependency: "direct main" description: name: http_mock_adapter - url: "https://pub.dartlang.org" + sha256: "0e7eaa5d77a273af1c2b5ec5066578faaa73039b63ccda5263c200756f24441a" + url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.4.2" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" intl: dependency: transitive description: name: intl - url: "https://pub.dartlang.org" + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" jmap_dart_client: dependency: "direct main" description: path: "." ref: master - resolved-ref: "45ea109f70be0d868b005aadf11a39e2ac816c38" + resolved-ref: e8005e28b48ee06259d4f51045a58f20c891e0b9 url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" @@ -428,72 +500,90 @@ packages: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.7" json_annotation: dependency: "direct main" description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.8.0" json_serializable: dependency: "direct dev" description: name: json_serializable - url: "https://pub.dartlang.org" + sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a + url: "https://pub.dev" + source: hosted + version: "6.6.1" + linkify: + dependency: transitive + description: + name: linkify + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "5.0.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.15" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.9.1" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" mockito: dependency: "direct dev" description: name: mockito - url: "https://pub.dartlang.org" + sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.4.2" model: dependency: "direct main" description: @@ -505,93 +595,162 @@ packages: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.1" - path_drawing: + version: "1.8.3" + path_parsing: dependency: transitive description: - name: path_drawing - url: "https://pub.dartlang.org" + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" source: hosted version: "1.0.1" - path_parsing: + path_provider: dependency: transitive description: - name: path_parsing - url: "https://pub.dartlang.org" + name: path_provider + sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.13" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + url: "https://pub.dev" + source: hosted + version: "2.0.27" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + url: "https://pub.dev" + source: hosted + version: "2.1.11" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: c2af5a8a6369992d915f8933dfc23172071001359d17896e83db8be57db8a397 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + url: "https://pub.dev" + source: hosted + version: "2.1.7" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pointer_interceptor: dependency: transitive description: name: pointer_interceptor - url: "https://pub.dartlang.org" + sha256: "49e6b86ba931d801ce852990d4a8913726ea3964266559e0b058baa3b4408435" + url: "https://pub.dev" source: hosted version: "0.9.1" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" source: hosted - version: "3.0.1+1" + version: "3.2.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" sky_engine: dependency: transitive description: flutter @@ -601,247 +760,258 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298 + url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.2.7" source_helper: dependency: transitive description: name: source_helper - url: "https://pub.dartlang.org" + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" source: hosted version: "1.3.3" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" - sqflite: - dependency: transitive - description: - name: sqflite - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0+4" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" - synchronized: - dependency: transitive - description: - name: synchronized - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0+3" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.5.1" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" universal_html: dependency: transitive description: name: universal_html - url: "https://pub.dartlang.org" + sha256: b5061c64c7c863c12e46279e032976f1c274f927fb3589b52b5928dcd2d52f7c + url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.0.9" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.dartlang.org" + sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.0" uri: dependency: transitive description: name: uri - url: "https://pub.dartlang.org" + sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" + url: "https://pub.dev" source: hosted version: "1.0.0" url_launcher: dependency: transitive description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" + url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.1.10" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: dd729390aa936bf1bdf5cd1bc7468ff340263f80a2c4f569416507667de8e3c8 + url: "https://pub.dev" source: hosted - version: "6.0.20" + version: "6.0.26" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: "3dedc66ca3c0bef9e6a93c0999aee102556a450afcc1b7bcfeace7a424927d92" + url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.1.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.4" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.4" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.16" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd + url: "https://pub.dev" source: hosted - version: "3.0.1" - vector_math: + version: "3.0.5" + vector_graphics: dependency: transitive description: - name: vector_math - url: "https://pub.dartlang.org" + name: vector_graphics + sha256: "4cf8e60dbe4d3a693d37dff11255a172594c0793da542183cbfe7fe978ae4aaa" + url: "https://pub.dev" source: hosted - version: "2.1.2" - watcher: + version: "1.1.4" + vector_graphics_codec: dependency: transitive description: - name: watcher - url: "https://pub.dartlang.org" + name: vector_graphics_codec + sha256: "278ad5f816f58b1967396d1f78ced470e3e58c9fe4b27010102c0a595c764468" + url: "https://pub.dev" source: hosted - version: "1.0.2" - web_socket_channel: + version: "1.1.4" + vector_graphics_compiler: dependency: transitive description: - name: web_socket_channel - url: "https://pub.dartlang.org" + name: vector_graphics_compiler + sha256: "0bf61ad56e6fd6688a2865d3ceaea396bc6a0a90ea0d7ad5049b1b76c09d6163" + url: "https://pub.dev" source: hosted - version: "2.2.0" - webview_flutter: + version: "1.1.4" + vector_math: dependency: transitive description: - name: webview_flutter - url: "https://pub.dartlang.org" + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "3.0.0" - webview_flutter_android: + version: "2.1.4" + watcher: dependency: transitive description: - name: webview_flutter_android - url: "https://pub.dartlang.org" + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted - version: "2.10.4" - webview_flutter_platform_interface: + version: "1.0.2" + web_socket_channel: dependency: transitive description: - name: webview_flutter_platform_interface - url: "https://pub.dartlang.org" + name: web_socket_channel + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" source: hosted - version: "1.9.5" - webview_flutter_wkwebview: + version: "2.3.0" + win32: dependency: transitive description: - name: webview_flutter_wkwebview - url: "https://pub.dartlang.org" + name: win32 + sha256: "5cdbe09a75b5f4517adf213c68aaf53ffa162fadf54ba16f663f94f3d2664a56" + url: "https://pub.dev" source: hosted - version: "2.9.5" - win32: + version: "4.1.1" + xdg_directories: dependency: transitive description: - name: win32 - url: "https://pub.dartlang.org" + name: xdg_directories + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "1.0.0" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.2" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.7.0" diff --git a/contact/pubspec.yaml b/contact/pubspec.yaml index 33b10417d9..97466ee95c 100644 --- a/contact/pubspec.yaml +++ b/contact/pubspec.yaml @@ -5,7 +5,7 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.16.2 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: @@ -14,28 +14,32 @@ dependencies: model: path: ../model - equatable: 2.0.3 - - json_annotation: 4.5.0 - + ### Dependencies from git ### jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git ref: master - http_mock_adapter: 0.3.2 + ### Dependencies from pub.dev ### + equatable: 2.0.5 + + json_annotation: 4.8.0 + + dio: 5.0.0 + + http_mock_adapter: 0.4.2 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: 1.0.4 + flutter_lints: 2.0.1 - build_runner: 2.1.11 + build_runner: 2.3.3 - json_serializable: 6.2.0 + json_serializable: 6.6.1 - mockito: 5.2.0 + mockito: 5.4.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/contact/test/contact/autocomplete_tmail_contact_method_test.dart b/contact/test/contact/autocomplete_tmail_contact_method_test.dart index 7c5e02aada..399823e7a2 100644 --- a/contact/test/contact/autocomplete_tmail_contact_method_test.dart +++ b/contact/test/contact/autocomplete_tmail_contact_method_test.dart @@ -81,8 +81,7 @@ void main() { }, headers: { "accept": "application/json;jmapVersion=rfc-8621", - "content-type": "application/json; charset=utf-8", - "content-length": 245 + "content-length": 330 }); final httpClient = HttpClient(dio); diff --git a/contact/test/datasource/tmail_contact_datasource_impl_test.dart b/contact/test/datasource/tmail_contact_datasource_impl_test.dart index 582b4a384b..8ccd0f75e4 100644 --- a/contact/test/datasource/tmail_contact_datasource_impl_test.dart +++ b/contact/test/datasource/tmail_contact_datasource_impl_test.dart @@ -28,22 +28,22 @@ void main() { ); group('tmail_contact_datasource_impl_test', () { - late ContactAPI _contactAPI; - late TMailContactDataSourceImpl _tmailContactDataSourceImpl; + late ContactAPI contactAPI; + late TMailContactDataSourceImpl tmailContactDataSourceImpl; setUp(() { - _contactAPI = MockContactAPI(); - _tmailContactDataSourceImpl = TMailContactDataSourceImpl(_contactAPI); + contactAPI = MockContactAPI(); + tmailContactDataSourceImpl = TMailContactDataSourceImpl(contactAPI); }); test('getAutoComplete should return success with valid data', () async { - when(_contactAPI.getAutoComplete( + when(contactAPI.getAutoComplete( AutoCompletePattern( word: 'marie', accountId: AccountId(Id('29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6'))) )).thenAnswer((_) async => [contact1, contact2]); - final result = await _tmailContactDataSourceImpl.getAutoComplete( + final result = await tmailContactDataSourceImpl.getAutoComplete( AutoCompletePattern( word: 'marie', accountId: AccountId(Id('29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6'))) diff --git a/core/analysis_options.yaml b/core/analysis_options.yaml index 61b6c4de17..0f32754d37 100644 --- a/core/analysis_options.yaml +++ b/core/analysis_options.yaml @@ -10,20 +10,7 @@ include: package:flutter_lints/flutter.yaml linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + constant_identifier_names: false + non_constant_identifier_names: false + unnecessary_string_escapes: false \ No newline at end of file diff --git a/core/lib/core.dart b/core/lib/core.dart index 6020093ddd..a98fe21bc6 100644 --- a/core/lib/core.dart +++ b/core/lib/core.dart @@ -3,6 +3,7 @@ library core; // Extensions export 'presentation/extensions/color_extension.dart'; export 'presentation/extensions/url_extension.dart'; +export 'presentation/extensions/uri_extension.dart'; export 'presentation/extensions/capitalize_extension.dart'; export 'presentation/extensions/list_extensions.dart'; export 'domain/extensions/datetime_extension.dart'; @@ -12,6 +13,7 @@ export 'presentation/extensions/compare_list_extensions.dart'; export 'presentation/extensions/string_extension.dart'; export 'presentation/extensions/tap_down_details_extension.dart'; export 'domain/extensions/media_type_extension.dart'; +export 'presentation/extensions/map_extensions.dart'; // Exceptions export 'domain/exceptions/download_file_exception.dart'; @@ -39,17 +41,19 @@ export 'utils/config/app_config_loader.dart'; export 'utils/config/app_config_parser.dart'; export 'utils/config/errors.dart'; export 'data/utils/compress_file_utils.dart'; +export 'utils/platform_info.dart'; // Views export 'presentation/views/text/slogan_builder.dart'; export 'presentation/views/text/text_field_builder.dart'; +export 'presentation/views/text/text_form_field_builder.dart'; export 'presentation/views/text/input_decoration_builder.dart'; -export 'presentation/views/text/text_builder.dart'; export 'presentation/views/text/rich_text_builder.dart'; +export 'presentation/views/text/text_overflow_builder.dart'; export 'presentation/views/responsive/responsive_widget.dart'; export 'presentation/views/list/tree_view.dart'; -export 'presentation/views/button/button_builder.dart'; export 'presentation/views/button/icon_button_web.dart'; +export 'presentation/views/button/tmail_button_widget.dart'; export 'presentation/views/image/avatar_builder.dart'; export 'presentation/views/list/sliver_grid_delegate_fixed_height.dart'; export 'presentation/views/image/icon_builder.dart'; @@ -61,7 +65,6 @@ export 'presentation/views/dialog/downloading_file_dialog_builder.dart'; export 'presentation/views/dialog/confirmation_dialog_builder.dart'; export 'presentation/views/dialog/edit_text_dialog_builder.dart'; export 'presentation/views/dialog/color_picker_dialog_builder.dart'; -export 'presentation/views/background/background_widget_builder.dart'; export 'presentation/views/html_viewer/html_content_viewer_widget.dart'; export 'presentation/views/html_viewer/html_content_viewer_on_web_widget.dart'; export 'presentation/views/html_viewer/html_viewer_controller_for_web.dart'; @@ -73,12 +76,14 @@ export 'presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder export 'presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart'; export 'presentation/views/search/search_bar_view.dart'; export 'presentation/views/popup_menu/popup_menu_item_widget.dart'; -export 'presentation/views/tab_bar/custom_tab_indicator.dart'; export 'presentation/views/quick_search/quick_search_input_form.dart'; export 'presentation/views/toast/toast_position.dart'; export 'presentation/views/toast/tmail_toast.dart'; export 'presentation/views/bottom_popup/full_screen_action_sheet_builder.dart'; export 'presentation/views/checkbox/labeled_checkbox.dart'; +export 'presentation/views/container/tmail_container_widget.dart'; +export 'presentation/views/clipper/side_arrow_clipper.dart'; +export 'presentation/views/avatar/gradient_circle_avatar_icon.dart'; // Resources export 'presentation/resources/assets_paths.dart'; @@ -95,14 +100,14 @@ export 'data/network/dio_client.dart'; export 'data/network/download/download_client.dart'; export 'data/network/download/download_manager.dart'; export 'data/network/download/downloaded_response.dart'; -export 'data/network/download/download_client.dart'; -export 'domain/exceptions/web_session_exception.dart'; // State export 'presentation/state/success.dart'; export 'presentation/state/failure.dart'; -export 'presentation/state/app_state.dart'; // Model export 'data/model/source_type/data_source_type.dart'; -export 'data/model/query/query_parameter.dart'; \ No newline at end of file +export 'data/model/query/query_parameter.dart'; + +// Action +export 'presentation/action/action_callback_define.dart'; \ No newline at end of file diff --git a/core/lib/data/model/source_type/data_source_type.dart b/core/lib/data/model/source_type/data_source_type.dart index 1d273da485..90ab33dd90 100644 --- a/core/lib/data/model/source_type/data_source_type.dart +++ b/core/lib/data/model/source_type/data_source_type.dart @@ -1,5 +1,6 @@ enum DataSourceType { network, - local + local, + hiveCache; } \ No newline at end of file diff --git a/core/lib/data/network/config/dynamic_url_interceptors.dart b/core/lib/data/network/config/dynamic_url_interceptors.dart index 1734808e2a..6a5389f0f9 100644 --- a/core/lib/data/network/config/dynamic_url_interceptors.dart +++ b/core/lib/data/network/config/dynamic_url_interceptors.dart @@ -1,12 +1,19 @@ import 'package:dio/dio.dart'; class DynamicUrlInterceptors extends InterceptorsWrapper { + String? _jmapUrl; String? _baseUrl; void changeBaseUrl(String? url) { _baseUrl = url; } + void setJmapUrl(String? url) { + _jmapUrl = url; + } + + String? get jmapUrl => _jmapUrl; + String? get baseUrl => _baseUrl; @override diff --git a/core/lib/data/network/dio_client.dart b/core/lib/data/network/dio_client.dart index b18d77a9e6..022f690a60 100644 --- a/core/lib/data/network/dio_client.dart +++ b/core/lib/data/network/dio_client.dart @@ -1,7 +1,7 @@ import 'dart:io'; +import 'package:core/data/extensions/options_extensions.dart'; import 'package:dio/dio.dart'; -import 'package:core/core.dart'; class DioClient { static const jmapHeader = 'application/json;jmapVersion=rfc-8621'; diff --git a/core/lib/data/network/download/download_client.dart b/core/lib/data/network/download/download_client.dart index b8da1522a3..31ad60439e 100644 --- a/core/lib/data/network/download/download_client.dart +++ b/core/lib/data/network/download/download_client.dart @@ -1,13 +1,12 @@ import 'dart:convert'; import 'dart:io'; -import 'dart:typed_data'; import 'package:core/data/network/dio_client.dart'; import 'package:core/data/utils/compress_file_utils.dart'; import 'package:core/presentation/extensions/html_extension.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; @@ -56,7 +55,7 @@ class DownloadClient { return null; } - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { final base64Uri = encodeToBase64Uri({ 'bytesData': bytesData, 'mimeType': 'image/$fileExtension', diff --git a/core/lib/data/network/download/download_manager.dart b/core/lib/data/network/download/download_manager.dart index a6a27cd444..c86eae4ad1 100644 --- a/core/lib/data/network/download/download_manager.dart +++ b/core/lib/data/network/download/download_manager.dart @@ -43,8 +43,8 @@ class DownloadManager { .takeWhile((_) => cancelToken == null || !cancelToken.isCancelled) .listen((data) { subscription.pause(); - randomAccessFile.writeFrom(data).then((_randomAccessFile) { - randomAccessFile = _randomAccessFile; + randomAccessFile.writeFrom(data).then((accessFile) { + randomAccessFile = accessFile; subscription.resume(); if (cancelToken != null && cancelToken.isCancelled) { streamController.sink.addError(CancelDownloadFileException(cancelToken.cancelError?.message)); diff --git a/core/lib/data/utils/device_manager.dart b/core/lib/data/utils/device_manager.dart index 5462b20faf..25cb3157a3 100644 --- a/core/lib/data/utils/device_manager.dart +++ b/core/lib/data/utils/device_manager.dart @@ -2,7 +2,6 @@ import 'dart:core'; import 'package:device_info_plus/device_info_plus.dart'; - class DeviceManager { final DeviceInfoPlugin _deviceInfoPlugin; @@ -11,9 +10,6 @@ class DeviceManager { Future isNeedRequestStoragePermissionOnAndroid() async { final androidInfo = await _deviceInfoPlugin.androidInfo; final sdkInt = androidInfo.version.sdkInt; - if (sdkInt != null) { - return sdkInt <= 28; - } - return false; + return sdkInt <= 28; } } \ No newline at end of file diff --git a/core/lib/domain/extensions/media_type_extension.dart b/core/lib/domain/extensions/media_type_extension.dart index 79e719af4b..b291d1d3d9 100644 --- a/core/lib/domain/extensions/media_type_extension.dart +++ b/core/lib/domain/extensions/media_type_extension.dart @@ -3,6 +3,8 @@ import 'package:core/domain/preview/supported_preview_file_types.dart'; import 'package:http_parser/http_parser.dart'; extension MediaTypeExtension on MediaType { + static const String imageType = 'image'; + bool isAndroidSupportedPreview() => SupportedPreviewFileTypes.androidSupportedTypes.contains(mimeType); bool isIOSSupportedPreview() => SupportedPreviewFileTypes.iOSSupportedTypes.containsKey(mimeType); diff --git a/core/lib/presentation/action/action_callback_define.dart b/core/lib/presentation/action/action_callback_define.dart new file mode 100644 index 0000000000..8bce05143e --- /dev/null +++ b/core/lib/presentation/action/action_callback_define.dart @@ -0,0 +1,5 @@ + +import 'package:flutter/material.dart'; + +typedef OnTapActionCallback = void Function(); +typedef OnTapActionAtPositionCallback = void Function(RelativeRect position); \ No newline at end of file diff --git a/core/lib/presentation/extensions/color_extension.dart b/core/lib/presentation/extensions/color_extension.dart index 99781b7a48..3143ae7733 100644 --- a/core/lib/presentation/extensions/color_extension.dart +++ b/core/lib/presentation/extensions/color_extension.dart @@ -1,5 +1,3 @@ -import 'dart:ui' show Color; - import 'package:flutter/material.dart'; extension AppColor on Color { @@ -59,15 +57,15 @@ extension AppColor on Color { static const enableSendEmailButtonColor = Color(0xFF007AFF); static const disableSendEmailButtonColor = Color(0xFFA9B4C2); static const borderLeftEmailContentColor = Color(0xFFEFEFEF); - static const toastBackgroundColor = Color(0xFFACAFFF); + static const toastWarningBackgroundColor = Color(0xFFFFC107); static const toastSuccessBackgroundColor = Color(0xFF4BB34B); - static const toastErrorBackgroundColor = Color(0xFFFF5858); + static const toastErrorBackgroundColor = Color(0xFFE64646); static const toastWithActionBackgroundColor = Color(0xFF3F3F3F); static const buttonActionToastWithActionColor = Color(0xFF7ADCF8); static const backgroundCountAttachment = Color(0x681C1C1C); static const bgStatusResultSearch = Color(0xFFF5F5F7); static const bgWordSearch = Color(0x3D007AFF); - static const lineItemListColor = Color(0xFF99A2AD); + static const lineItemListColor = Color(0xFFE7E8EC); static const colorNameEmail = Color(0xFF000000); static const colorContentEmail = Color(0xFF6D7885); static const colorTextButton = Color(0xFF007AFF); @@ -75,7 +73,7 @@ extension AppColor on Color { static const colorBgSearchBar = Color(0x99EBEDF0); static const colorBgIdentityButton = Color(0x00EBEDF0); static const colorShadowBgContentEmail = Color(0x14000000); - static const colorDividerMailbox = Color(0xFF99A2AD); + static const colorDividerMailbox = Color(0x1F000000); static const colorCollapseMailbox = Color(0xFFB8C1CC); static const colorExpandMailbox = Color(0xFF007AFF); static const colorBgMailbox = Color(0xFFF7F7F7); @@ -149,6 +147,7 @@ extension AppColor on Color { static const colorCloseButton = Color(0xFF818C99); static const colorDropShadow = Color(0x0F000000); static const colorBackgroundKeyboard = Color(0xFFD2D5DC); + static const colorBackgroundKeyboardAndroid = Color(0xFFF2F0F4); static const colorShadowLayerBottom = Color(0x29000000); static const colorShadowLayerTop = Color(0x1F000000); static const colorDividerHorizontal = Color(0x1F000000); @@ -165,16 +164,62 @@ extension AppColor on Color { static const colorDeleteContactIcon = Color(0xFFAEB7C2); static const colorItemRecipientSelected = Color(0xFFDFEEFF); static const colorBackgroundQuotasWarning = Color(0xFFFFC107); - static const colorTitleQuotasWarning = Color(0xFFF05C44); - static const colorProgressQuotasWarning = Color(0xFFFFA000); - static const colorOutOfStorageQuotasWarning = Color(0xffE64646); + static const colorQuotaWarning = Color(0xFFF05C44); + static const colorQuotaError = Color(0xffE64646); static const colorThumbScrollBar = Color(0xFFAEB7C2); static const colorCreateNewIdentityButton = Color(0xFFEBEDF0); - static const colorSpamReportBox = Color(0xFFBFDEFF); + static const colorSpamReportBannerBackground = Color(0xFFBFDEFF); + static const colorSpamReportBannerStrokeBorder = Color(0x1F000000); + static const colorSpamReportBannerLabelColor = Color(0xFF626D7A); + static const colorSpamReportBannerButtonBackground = Color(0xFFEBEDF0); static const colorSubtitle = Color(0xFF6D7885); static const colorBackgroundSearchMailboxInput = Color(0xFFEBEDF0); static const colorMailboxHovered = Color(0xFFEBEDF0); static const colorMailboxPath = Color(0xFF818C99); + static const colorIconUnSubscribedMailbox = Color(0xFFAEB7C2); + static const colorTitleAUnSubscribedMailbox = Color(0xFF818C99); + static const colorTitleSendingItem = Color(0xFF818C99); + static const colorBannerMessageSendingQueue = Color(0xFFF7F8FA); + static const colorDeliveringState = Color(0xFFAEB7C2); + static const colorErrorState = Color(0xFFE64646); + static const colorBackgroundErrorState = Color(0xFFFAEBEB); + static const colorBackgroundDeliveringState = Color(0xFFF2F3F5); + static const colorNetworkConnectionBannerBackground = Color(0x99EBEDF0); + static const colorNetworkConnectionLabel = Color(0xFF818C99); + static const colorCalendarEventRead = Color(0xFF818C99); + static const colorCalendarEventUnread = Color(0xFF1C1B1F); + static const colorMaybeEventActionText = Color(0xFFFFC107); + static const colorInvitedEventActionText = Color(0xFF007AFF); + static const colorUpdatedEventActionText = Color(0xFF4BB34B); + static const colorCanceledEventActionText = Color(0xFFFF3347); + static const colorSubTitleEventActionText = Color(0xFF939393); + static const colorCalendarEventInformationBackground = Color(0x0A000000); + static const colorCalendarEventInformationStroke = Color(0x1F000000); + static const colorShadowCalendarDateIcon = Color(0x26000000); + static const colorOrganizerMailto = Color(0xFFB3B3B3); + static const colorMailto = Color(0xFFB3B3B3); + static const colorEventDescriptionBackground = Color(0x05000000); + static const colorLabelQuotas = Color(0xFF818C99); + static const colorLabelCancelButton = Color(0xFFAEB7C2); + static const colorCreateFiltersButton = Color(0xFFF3F3F7); + static const colorTextBody = Color(0xFF818C99); + static const colorClosePopupDialogButton = Color(0xFFAEB7C2); + static const colorEmptyPopupDialogButton = Color(0xFFFFC107); + static const colorCancelPopupDialogButton = Color(0xFFEBEDF0); + static const colorRemoveRuleFilterConditionButton = Color(0xFFE6E8EC); + static const colorComposerShadowTop = Color(0x28000000); + static const colorComposerShadowBottom = Color(0x1E000000); + static const colorComposerAppBar = Color(0xFFF4F4F4); + static const colorLabelComposer = Color(0xFF8C9CAF); + static const colorLineComposer = Color(0xFFF4F4F4); + static const colorPrefixButtonComposer = Color(0xFFAEAAAE); + static const colorRichButtonComposer = Color(0xFFAEAEC0); + static const colorMobileRichButtonComposer = Color(0xFF8C9CAF); + static const colorSelected = Color(0xFFE3F1FF); + static const colorAttachmentBorder = Color(0xFFE5ECF3); + static const colorProgressLoadingBackground = Color(0xFFE3F1FF); + static const colorDropZoneBackground = Color(0xFFF6FAFF); + static const colorDropZoneBorder = Color(0xFF46A2FF); static const mapGradientColor = [ [Color(0xFF21D4FD), Color(0xFFB721FF)], @@ -193,5 +238,67 @@ extension AppColor on Color { .toRadixString(16) .padLeft(6, '0') .toUpperCase()}'; + + static List get listColorsPicker { + return [ + ...Colors.grey.listShadeColors, + ...listMaterialColors.map((color) => color.shade900).toList(), + ...listMaterialColors.map((color) => color.shade800).toList(), + ...listMaterialColors.map((color) => color.shade700).toList(), + ...listMaterialColors.map((color) => color.shade600).toList(), + ...listMaterialColors.map((color) => color.shade500).toList(), + ...listMaterialColors.map((color) => color.shade400).toList(), + ...listMaterialColors.map((color) => color.shade300).toList(), + ...listMaterialColors.map((color) => color.shade200).toList(), + ...listMaterialColors.map((color) => color.shade100).toList(), + ...listMaterialColors.map((color) => color.shade50).toList(), + ]; + } + + static List get listMaterialColors { + return [ + Colors.cyan, + Colors.blue, + Colors.deepPurple, + Colors.purple, + Colors.pink, + Colors.red, + Colors.deepOrange, + Colors.orange, + Colors.amber, + Colors.yellow, + Colors.lime, + Colors.green + ]; + } } +extension ColorNullableExtension on Color? { + ColorFilter? asFilter({BlendMode? blendMode}) { + if (this == null) { + return null; + } else { + return ColorFilter.mode(this!, blendMode ?? BlendMode.srcIn); + } + } +} + +extension MaterialColorExtension on MaterialColor { + + List get listShadeColors { + return [ + const Color(0xFFFFFFFF), + const Color(0xFFFCFCFC), + shade50, + shade100, + shade200, + shade300, + shade400, + shade500, + shade600, + shade700, + shade800, + shade900 + ]; + } +} \ No newline at end of file diff --git a/core/lib/presentation/extensions/html_extension.dart b/core/lib/presentation/extensions/html_extension.dart index abbc185050..c2195d2240 100644 --- a/core/lib/presentation/extensions/html_extension.dart +++ b/core/lib/presentation/extensions/html_extension.dart @@ -1,7 +1,8 @@ extension HtmlExtension on String { - static const String editorStartTags = '


'; + static const String editorStartTags = '

'; + static const String signaturePrefix = '-- '; String addBlockTag(String tag, {String? attribute}) => attribute != null @@ -24,10 +25,9 @@ extension HtmlExtension on String { 'blockquote', attribute: 'style="margin-left:8px;margin-right:8px;padding-left:12px;padding-right:12px;border-left:5px solid #eee;"'); - String asSignatureHtml() => '--

$this'; + String signaturePrefixTagHtml() => '$signaturePrefix'; - String toSignatureBlock() => - '
${asSignatureHtml()}

'; + String asSignatureHtml() => '${signaturePrefixTagHtml()}
$this
'; String removeEditorStartTag() { if (trim() == editorStartTags) { @@ -35,4 +35,9 @@ extension HtmlExtension on String { } return this; } + + String addCiteTag() => addBlockTag( + 'cite', + attribute: 'style="text-align: left;display: block;"' + ); } \ No newline at end of file diff --git a/core/lib/presentation/extensions/map_extensions.dart b/core/lib/presentation/extensions/map_extensions.dart new file mode 100644 index 0000000000..a741acb16f --- /dev/null +++ b/core/lib/presentation/extensions/map_extensions.dart @@ -0,0 +1,13 @@ + +extension MapExtensions on Map { + + Map where(bool Function(K, V) condition) { + Map result = {}; + for (var element in entries) { + if (condition(element.key, element.value)) { + result[element.key] = element.value; + } + } + return result; + } +} diff --git a/core/lib/presentation/extensions/string_extension.dart b/core/lib/presentation/extensions/string_extension.dart index cd4cd1502d..544db2b584 100644 --- a/core/lib/presentation/extensions/string_extension.dart +++ b/core/lib/presentation/extensions/string_extension.dart @@ -1,4 +1,5 @@ import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; extension StringExtension on String { @@ -38,4 +39,11 @@ extension StringExtension on String { return ''; } } + + String get overflow { + return characters + .replaceAll(Characters(''), Characters('\u{200B}')) + .replaceAll(Characters('-'), Characters('\u{2011}')) + .toString(); + } } \ No newline at end of file diff --git a/core/lib/presentation/extensions/uri_extension.dart b/core/lib/presentation/extensions/uri_extension.dart new file mode 100644 index 0000000000..9c65a6e367 --- /dev/null +++ b/core/lib/presentation/extensions/uri_extension.dart @@ -0,0 +1,21 @@ +import 'package:core/presentation/extensions/url_extension.dart'; +import 'package:core/utils/app_logger.dart'; + +extension URIExtension on Uri { + + Uri toQualifiedUrl({required Uri baseUrl}) { + log('SessionUtils::toQualifiedUrl():baseUrl: $baseUrl | sourceUrl: $this'); + if (toString().startsWith(baseUrl.toString())) { + final qualifiedUrl = toString().removeLastSlashOfUrl(); + log('SessionUtils::toQualifiedUrl():qualifiedUrl: $qualifiedUrl'); + return Uri.parse(qualifiedUrl); + } else { + final baseUrlValid = baseUrl.toString().removeLastSlashOfUrl(); + final sourceUrlValid = toString().addFirstSlashOfUrl().removeLastSlashOfUrl(); + log('SessionUtils::toQualifiedUrl():baseUrlValid: $baseUrlValid | sourceUrlValid: $sourceUrlValid'); + final qualifiedUrl = baseUrlValid + sourceUrlValid; + log('SessionUtils::toQualifiedUrl():qualifiedUrl: $qualifiedUrl'); + return Uri.parse(qualifiedUrl); + } + } +} \ No newline at end of file diff --git a/core/lib/presentation/extensions/url_extension.dart b/core/lib/presentation/extensions/url_extension.dart index 8f6a9059fe..7211a72800 100644 --- a/core/lib/presentation/extensions/url_extension.dart +++ b/core/lib/presentation/extensions/url_extension.dart @@ -11,7 +11,7 @@ extension URLExtension on String { } else if (startsWith(prefixUrlHttp)) { return kReleaseMode ? replaceAll(prefixUrlHttp, prefixUrlHttps) : this; } else { - return '$prefixUrlHttps${this}'; + return '$prefixUrlHttps$this'; } } return ''; @@ -28,4 +28,22 @@ extension URLExtension on String { } bool isValid() => startsWith(prefixUrlHttps) || startsWith(prefixUrlHttp); + + String removeLastSlashOfUrl() { + final lastSlash = lastIndexOf('/'); + if (lastSlash == length - 1) { + return substring(0, lastSlash); + } else { + return this; + } + } + + String addFirstSlashOfUrl() { + final firstSlash = indexOf('/'); + if (firstSlash == 0) { + return this; + } else { + return '/$this'; + } + } } \ No newline at end of file diff --git a/core/lib/presentation/resources/image_paths.dart b/core/lib/presentation/resources/image_paths.dart index 7084a8f7ba..d8b866c66c 100644 --- a/core/lib/presentation/resources/image_paths.dart +++ b/core/lib/presentation/resources/image_paths.dart @@ -35,7 +35,7 @@ class ImagePaths { String get icForward => _getImagePath('ic_forward.svg'); String get icMoveEmail => _getImagePath('ic_move_email.svg'); String get icUnreadEmail => _getImagePath('ic_unread_email.svg'); - String get icCloseMailbox => _getImagePath('ic_close_mailbox.svg'); + String get icCircleClose => _getImagePath('ic_circle_close.svg'); String get icAddNewFolder => _getImagePath('ic_add_new_folder.svg'); String get icFolderMailbox => _getImagePath('ic_folder_mailbox.svg'); String get icMailboxInbox => _getImagePath('ic_mailbox_inbox.svg'); @@ -47,17 +47,15 @@ class ImagePaths { String get icFilterSelected => _getImagePath('ic_filter_selected.svg'); String get icFilterMessageAll => _getImagePath('ic_filter_message_all.svg'); String get icFilterMessageAttachments => _getImagePath('ic_filter_message_attachments.svg'); - String get icLogoTMail => _getImagePath('logo_tmail.png'); String get icSendToast => _getImagePath('ic_send_toast.svg'); + String get icSendSuccessToast => _getImagePath('ic_send_success_toast.svg'); String get icClearTextSearch => _getImagePath('ic_clear_text_search.svg'); String get icRenameMailbox => _getImagePath('ic_rename_mailbox.svg'); String get icDeleteToast => _getImagePath('ic_delete_toast.svg'); String get icRemoveDialog => _getImagePath('ic_remove_dialog.svg'); String get icNewMessage => _getImagePath('ic_new_message.svg'); String get icUnreadStatus => _getImagePath('ic_unread_status.svg'); - String get icAttachmentsComposer => _getImagePath('ic_attachments_composer.svg'); - String get icCloseComposer => _getImagePath('ic_close_composer.svg'); - String get icFullScreenComposer => _getImagePath('ic_fullscreen_composer.svg'); + String get icFullScreen => _getImagePath('ic_fullscreen.svg'); String get icMinimize => _getImagePath('ic_minimize.svg'); String get icDeleteComposer => _getImagePath('ic_delete_composer.svg'); String get icDeleteAttachment => _getImagePath('ic_delete_attachment.svg'); @@ -96,7 +94,6 @@ class ImagePaths { String get icEncrypted => _getImagePath('ic_encrypted.svg'); String get icIntegration => _getImagePath('ic_integration.svg'); String get icTeam => _getImagePath('ic_team.svg'); - String get loginGraphic => _getImagePath('login_graphic.png'); String get icPowerByLinagora => _getImagePath('power_by_linagora.svg'); String get icAttachmentSB => _getImagePath('ic_attachment_sb.svg'); String get icCalendarSB => _getImagePath('ic_calendar_sb.svg'); @@ -111,7 +108,6 @@ class ImagePaths { String get icFilePdf => _getImagePath('ic_file_pdf.svg'); String get icFilePptx => _getImagePath('ic_file_pptx.svg'); String get icFileEPup => _getImagePath('ic_file_epup.svg'); - String get icCloseAdvancedSearch => _getImagePath('ic_close_advanced_search.svg'); String get icLanguage => _getImagePath('ic_language.svg'); String get icChecked => _getImagePath('ic_checked.svg'); String get icStyleBold => _getImagePath('ic_style_bold.svg'); @@ -175,6 +171,39 @@ class ImagePaths { String get icHideMailbox => _getImagePath('ic_hide_mailbox.svg'); String get icShowMailbox => _getImagePath('ic_show_mailbox.svg'); String get icMailboxVisibility => _getImagePath('ic_mailbox_visibility.svg'); + String get icToastSuccessMessage => _getImagePath('ic_toast_success_message.svg'); + String get icForwarded => _getImagePath('ic_forwarded.svg'); + String get icReplyAndForward => _getImagePath('ic_reply_and_forward.svg'); + String get icMailboxSendingQueue => _getImagePath('ic_mailbox_sending_queue.svg'); + String get icMailboxOutbox => _getImagePath('ic_mailbox_outbox.svg'); + String get icAvatarGroup => _getImagePath('ic_avatar_group.svg'); + String get icAvatarGroupDelivering => _getImagePath('ic_avatar_group_delivering.svg'); + String get icAvatarPersonal => _getImagePath('ic_avatar_personal.svg'); + String get icAvatarPersonalDelivering => _getImagePath('ic_avatar_personal_delivering.svg'); + String get icDialogOfflineMode => _getImagePath('ic_dialog_offline_mode.svg'); + String get icMenuMailbox => _getImagePath('ic_menu_mailbox.svg'); + String get icDelivering => _getImagePath('ic_delivering.svg'); + String get icError => _getImagePath('ic_error.svg'); + String get icConnectedInternet => _getImagePath('ic_connected_internet.svg'); + String get icArrowBottom => _getImagePath('ic_arrow_bottom.svg'); + String get icArrowLeft => _getImagePath('ic_arrow_left.svg'); + String get icArrowRight => _getImagePath('ic_arrow_right.svg'); + String get icAddPicture => _getImagePath('ic_add_picture.svg'); + String get icCalendarEvent => _getImagePath('ic_calendar_event.svg'); + String get icEventInvited => _getImagePath('ic_event_invited.svg'); + String get icEventUpdated => _getImagePath('ic_event_updated.svg'); + String get icEventCanceled => _getImagePath('ic_event_canceled.svg'); + String get icFormatQuote => _getImagePath('ic_format_quote.svg'); + String get icTMailLogo => _getImagePath('ic_tmail_logo.svg'); + String get icLoginGraphic => _getImagePath('ic_login_graphic.svg'); + String get icCancel => _getImagePath('ic_cancel.svg'); + String get icRichToolbar => _getImagePath('ic_rich_toolbar.svg'); + String get icSaveToDraft => _getImagePath('ic_save_to_draft.svg'); + String get icAttachmentFile => _getImagePath('ic_attachment_file.svg'); + String get icAttachFile => _getImagePath('ic_attach_file.svg'); + String get icSend => _getImagePath('ic_send.svg'); + String get icReadReceipt => _getImagePath('ic_read_receipt.svg'); + String get icDropZoneIcon => _getImagePath('ic_drop_zone_icon.svg'); String _getImagePath(String imageName) { return AssetsPaths.images + imageName; diff --git a/core/lib/presentation/state/app_state.dart b/core/lib/presentation/state/app_state.dart deleted file mode 100644 index 37f9b1006f..0000000000 --- a/core/lib/presentation/state/app_state.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:core/core.dart'; -import 'package:dartz/dartz.dart'; -import 'package:meta/meta.dart'; -import 'package:equatable/equatable.dart'; - -@immutable -abstract class AppState with EquatableMixin { - final Either viewState; - - AppState(this.viewState); - - @override - List get props => [viewState]; -} \ No newline at end of file diff --git a/core/lib/presentation/state/failure.dart b/core/lib/presentation/state/failure.dart index fc0118a58d..9ea7d6a707 100644 --- a/core/lib/presentation/state/failure.dart +++ b/core/lib/presentation/state/failure.dart @@ -1,5 +1,16 @@ import 'package:equatable/equatable.dart'; -abstract class Failure extends Equatable {} +abstract class Failure with EquatableMixin { -abstract class FeatureFailure extends Failure {} + @override + bool? get stringify => true; +} + +abstract class FeatureFailure extends Failure { + final dynamic exception; + + FeatureFailure({this.exception}); + + @override + List get props => [exception]; +} diff --git a/core/lib/presentation/state/success.dart b/core/lib/presentation/state/success.dart index d6061aa832..f25870d7ab 100644 --- a/core/lib/presentation/state/success.dart +++ b/core/lib/presentation/state/success.dart @@ -1,6 +1,10 @@ import 'package:equatable/equatable.dart'; -abstract class Success with EquatableMixin {} +abstract class Success with EquatableMixin { + + @override + bool? get stringify => true; +} abstract class ViewState extends Success {} @@ -15,23 +19,4 @@ class UIState extends ViewState { List get props => []; } -class LoadingState extends UIState { - LoadingState(); - - @override - List get props => []; -} - -class LoadingMoreState extends UIState { - LoadingMoreState(); - - @override - List get props => []; -} - -class RefreshingState extends UIState { - RefreshingState(); - - @override - List get props => []; -} \ No newline at end of file +class LoadingState extends UIState {} \ No newline at end of file diff --git a/core/lib/presentation/utils/app_toast.dart b/core/lib/presentation/utils/app_toast.dart index 3b95bdeeb2..b1bbf0aa5b 100644 --- a/core/lib/presentation/utils/app_toast.dart +++ b/core/lib/presentation/utils/app_toast.dart @@ -1,79 +1,124 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/toast/tmail_toast.dart'; +import 'package:core/presentation/views/toast/toast_position.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; class AppToast { - final fToast = Get.find(); - void showToast(String message) { - Fluttertoast.showToast( - msg: message, - fontSize: 16, - textColor: Colors.white, - backgroundColor: AppColor.toastBackgroundColor, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM); + void showToastErrorMessage( + BuildContext context, + String message, + { + Color? leadingSVGIconColor, + String? leadingSVGIcon, + Duration? duration, + } + ) { + final imagePaths = Get.find(); + showToastMessage( + context, + message, + backgroundColor: AppColor.toastErrorBackgroundColor, + textColor: Colors.white, + leadingSVGIconColor: leadingSVGIconColor ?? (leadingSVGIcon == null ? Colors.white : null), + leadingSVGIcon: leadingSVGIcon ?? imagePaths.icNotConnection, + duration: duration + ); } - void showSuccessToast(String message, {bool isToastLengthLong = false}) { - Fluttertoast.showToast( - msg: message, - fontSize: 16, - textColor: Colors.white, - backgroundColor: AppColor.toastSuccessBackgroundColor, - toastLength: isToastLengthLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM); + void showToastSuccessMessage( + BuildContext context, + String message, + { + Color? leadingSVGIconColor, + String? leadingSVGIcon, + Duration? duration, + } + ) { + final imagePaths = Get.find(); + showToastMessage( + context, + message, + backgroundColor: AppColor.toastSuccessBackgroundColor, + textColor: Colors.white, + leadingSVGIconColor: leadingSVGIconColor ?? (leadingSVGIcon == null ? Colors.white : null), + leadingSVGIcon: leadingSVGIcon ?? imagePaths.icToastSuccessMessage, + duration: duration + ); } - void showErrorToast(String message, {bool isToastLengthLong = false}) { - Fluttertoast.showToast( - msg: message, - fontSize: 16, - textColor: Colors.white, - backgroundColor: AppColor.toastErrorBackgroundColor, - toastLength: isToastLengthLong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM); + void showToastWarningMessage( + BuildContext context, + String message, + { + Color? leadingSVGIconColor, + String? leadingSVGIcon, + Duration? duration, + } + ) { + final imagePaths = Get.find(); + showToastMessage( + context, + message, + backgroundColor: AppColor.toastWarningBackgroundColor, + textColor: Colors.white, + leadingSVGIconColor: leadingSVGIconColor ?? (leadingSVGIcon == null ? Colors.white : null), + leadingSVGIcon: leadingSVGIcon ?? imagePaths.icInfoCircleOutline, + duration: duration + ); } - void showBottomToast( - BuildContext context, - String message, - { - String? actionName, - Function? onActionClick, - Widget? actionIcon, - Widget? leadingIcon, - double? maxWidth, - bool infinityToast = false, - Color? backgroundColor, - Color? textColor, - Color? textActionColor, - } + void showToastMessage( + BuildContext context, + String message, + { + String? actionName, + Function? onActionClick, + Widget? actionIcon, + Widget? leadingIcon, + String? leadingSVGIcon, + Color? leadingSVGIconColor, + double? maxWidth, + bool infinityToast = false, + Color? backgroundColor, + Color? textColor, + Color? textActionColor, + TextStyle? textStyle, + EdgeInsets? padding, + TextAlign? textAlign, + Duration? duration, + } ) { - Widget? trailingAction; - + final responsiveUtils = Get.find(); + Widget? trailingWidget; if (actionName != null) { if (actionIcon == null) { - trailingAction = TextButton( - onPressed: () { - ToastView.dismiss(); - onActionClick?.call(); - }, - child: Text( - actionName, - style: TextStyle( + trailingWidget = PointerInterceptor( + child: TextButton( + onPressed: () { + ToastView.dismiss(); + onActionClick?.call(); + }, + child: Text( + actionName, + style: TextStyle( fontSize: 15, fontWeight: FontWeight.normal, - color: textActionColor ?? AppColor.buttonActionToastWithActionColor), + color: textActionColor ?? Colors.white + ), + ), ), ); } else { - trailingAction = Material( - color: Colors.transparent, - child: InkWell( + trailingWidget = PointerInterceptor( + child: Material( + color: Colors.transparent, + child: InkWell( onTap: () { ToastView.dismiss(); onActionClick?.call(); @@ -82,109 +127,61 @@ class AppToast { child: Padding( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - actionIcon, - Text( - actionName, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: textActionColor ?? Colors.white), - ) - ] + mainAxisSize: MainAxisSize.min, + children: [ + actionIcon, + Text( + actionName, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: textActionColor ?? Colors.white + ), + ) + ] ), - )), + ) + ), + ), ); } } - showToastMessage( - context, - message, - maxWidth: maxWidth, - backgroundColor: backgroundColor, - infinityToast: infinityToast, - textColor: textColor, - leading: leadingIcon, - trailing: trailingAction); - } + Widget? leadingWidget; + if (leadingIcon != null) { + leadingWidget = PointerInterceptor(child: leadingIcon); + } else { + if (leadingSVGIcon != null) { + leadingWidget = PointerInterceptor( + child: SvgPicture.asset( + leadingSVGIcon, + width: 24, + height: 24, + fit: BoxFit.fill, + colorFilter: leadingSVGIconColor?.asFilter(), + ) + ); + } + } - void showToastMessage( - BuildContext context, - String message, { - Color? textColor, - Widget? leading, - bool infinityToast = false, - Widget? trailing, - double? maxWidth, - Color? backgroundColor - }) { TMailToast.showToast( - message, - context, - width: maxWidth, - toastPosition: ToastPosition.BOTTOM, - textStyle: TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: textColor ?? Colors.white), - backgroundColor: backgroundColor ?? AppColor.toastWithActionBackgroundColor, - trailing: trailing != null - ? Padding( - padding: const EdgeInsets.only(left: 8), - child: PointerInterceptor(child: trailing)) - : null, - leading: leading != null - ? Padding( - padding: const EdgeInsets.only(right: 8), - child: PointerInterceptor(child: leading)) - : null, - toastBorderRadius: 10.0, - toastDuration: infinityToast ? null : 3, - ); - } - - void showToastWithIcon(BuildContext context, { - String? message, String? icon, Color? bgColor, Color? iconColor, - Color? textColor, double? radius, EdgeInsets? padding, - TextStyle? textStyle, double? widthToast, Duration? toastLength}) { - double sizeWidth = context.width > 360 ? 360 : context.width - 40; - final toast = Material( - color: bgColor ?? Colors.white, - elevation: 10, - shadowColor: Colors.black54, - borderRadius: BorderRadius.all(Radius.circular(radius ?? 10)), - child: Container( - padding: padding ?? const EdgeInsets.symmetric(horizontal: 12.0, vertical: 14), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(radius ?? 10.0), - color: bgColor ?? Colors.white, - ), - width: widthToast ?? sizeWidth, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (icon != null) - SvgPicture.asset(icon, - width: 24, - height: 24, - fit: BoxFit.fill, - color: iconColor), - if (icon != null) - const SizedBox(width: 10.0), - Expanded(child: Text( - message ?? '', - style: textStyle ?? TextStyle(fontSize: 15, color: textColor ?? Colors.black))), - ], - ), + message, + context, + maxWidth: maxWidth ?? responsiveUtils.getMaxWidthToast(context), + toastPosition: ToastPosition.BOTTOM, + textStyle: textStyle ?? TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + color: textColor ?? AppColor.primaryColor ), - ); - fToast.init(context); - fToast.showToast( - child: toast, - gravity: ToastGravity.BOTTOM, - toastDuration: toastLength ?? const Duration(seconds: 3), + backgroundColor: backgroundColor ?? Colors.white, + trailing: trailingWidget, + leading: leadingWidget, + padding: padding, + textAlign: textAlign, + toastDuration: infinityToast + ? null + : (duration ?? const Duration(seconds: 3)), ); } } diff --git a/core/lib/presentation/utils/html_transformer/base/dom_transformer.dart b/core/lib/presentation/utils/html_transformer/base/dom_transformer.dart index 32d0f332cf..66922d4412 100644 --- a/core/lib/presentation/utils/html_transformer/base/dom_transformer.dart +++ b/core/lib/presentation/utils/html_transformer/base/dom_transformer.dart @@ -1,5 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/data/network/dio_client.dart'; import 'package:html/dom.dart'; /// Transforms the HTML DOM. @@ -10,13 +10,11 @@ abstract class DomTransformer { /// Uses the `DOM` [document] to transform the `document`. /// /// All changes will be visible to subsequent transformers. - Future process( - Document document, - { - Map? mapUrlDownloadCID, - DioClient? dioClient, - } - ); + Future process({ + required Document document, + required DioClient dioClient, + Map? mapUrlDownloadCID, + }); /// Adds a HEAD element if necessary void ensureDocumentHeadIsAvailable(Document document) { diff --git a/core/lib/presentation/utils/html_transformer/base/text_transformer.dart b/core/lib/presentation/utils/html_transformer/base/text_transformer.dart index af064550d8..e7100b8b14 100644 --- a/core/lib/presentation/utils/html_transformer/base/text_transformer.dart +++ b/core/lib/presentation/utils/html_transformer/base/text_transformer.dart @@ -1,7 +1,9 @@ +import 'dart:convert'; + /// Transforms plain text messages. abstract class TextTransformer { const TextTransformer(); - String process(String text); + String process(String text, HtmlEscape htmlEscape); } \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/dom/add_target_blank_in_tag_a_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/add_target_blank_in_tag_a_transformers.dart index d9454297dd..a66a387d92 100644 --- a/core/lib/presentation/utils/html_transformer/dom/add_target_blank_in_tag_a_transformers.dart +++ b/core/lib/presentation/utils/html_transformer/dom/add_target_blank_in_tag_a_transformers.dart @@ -6,14 +6,18 @@ class AddTargetBlankInTagATransformer extends DomTransformer { const AddTargetBlankInTagATransformer(); @override - Future process( - Document document, { + Future process({ + required Document document, + required DioClient dioClient, Map? mapUrlDownloadCID, - DioClient? dioClient, }) async { final elements = document.querySelectorAll('a'); await Future.wait(elements.map((element) async { element.attributes['target'] = '_blank'; + final rel = element.attributes['rel']; + if (rel == null || (!rel.contains('noopener') && !rel.contains('noreferrer'))) { + element.attributes['rel'] = 'noreferrer'; + } })); } } diff --git a/core/lib/presentation/utils/html_transformer/dom/add_tooltip_link_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/add_tooltip_link_transformers.dart index a085814ea8..aea1d9ccaa 100644 --- a/core/lib/presentation/utils/html_transformer/dom/add_tooltip_link_transformers.dart +++ b/core/lib/presentation/utils/html_transformer/dom/add_tooltip_link_transformers.dart @@ -1,22 +1,19 @@ import 'package:core/data/network/dio_client.dart'; import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/html_template.dart'; import 'package:html/dom.dart'; -import '../html_template.dart'; - class AddTooltipLinkTransformer extends DomTransformer { const AddTooltipLinkTransformer(); @override - Future process( - Document document, - { - Map? mapUrlDownloadCID, - DioClient? dioClient - } - ) async { + Future process({ + required Document document, + required DioClient dioClient, + Map? mapUrlDownloadCID, + }) async { final linkElements = document.querySelectorAll('a[href^="http"]'); await Future.wait(linkElements.map((linkElement) async { _addToolTipWhenHoverLink(linkElement); @@ -30,7 +27,11 @@ class AddTooltipLinkTransformer extends DomTransformer { if (children.isEmpty && text.isNotEmpty && url != null) { final innerHtml = element.innerHtml; final tagClass = element.attributes['class']; - element.attributes['class'] = '$tagClass $nameClassToolTip'; + if (tagClass != null) { + element.attributes['class'] = '$tagClass $nameClassToolTip'; + } else { + element.attributes['class'] = nameClassToolTip; + } element.innerHtml = innerHtml + textHasToolTip(url); } } diff --git a/core/lib/presentation/utils/html_transformer/dom/blockcode_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/blockcode_transformers.dart index 8f7e710261..bc8dab461c 100644 --- a/core/lib/presentation/utils/html_transformer/dom/blockcode_transformers.dart +++ b/core/lib/presentation/utils/html_transformer/dom/blockcode_transformers.dart @@ -8,13 +8,11 @@ class BlockCodeTransformer extends DomTransformer { const BlockCodeTransformer(); @override - Future process( - Document document, - { - Map? mapUrlDownloadCID, - DioClient? dioClient - } - ) async { + Future process({ + required Document document, + required DioClient dioClient, + Map? mapUrlDownloadCID, + }) async { final codeElements = document.getElementsByTagName('pre'); await Future.wait(codeElements.map((element) async { element.attributes['style'] = ''' diff --git a/core/lib/presentation/utils/html_transformer/dom/blockquoted_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/blockquoted_transformers.dart index 7c8df307c0..e447aad75b 100644 --- a/core/lib/presentation/utils/html_transformer/dom/blockquoted_transformers.dart +++ b/core/lib/presentation/utils/html_transformer/dom/blockquoted_transformers.dart @@ -8,13 +8,11 @@ class BlockQuotedTransformer extends DomTransformer { const BlockQuotedTransformer(); @override - Future process( - Document document, - { - Map? mapUrlDownloadCID, - DioClient? dioClient - } - ) async { + Future process({ + required Document document, + required DioClient dioClient, + Map? mapUrlDownloadCID, + }) async { final quotedElements = document.getElementsByTagName('blockquote'); await Future.wait(quotedElements.map((quotedElement) async { quotedElement.attributes['style'] = ''' diff --git a/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart index 22d00f12dc..e8ee882d2d 100644 --- a/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart +++ b/core/lib/presentation/utils/html_transformer/dom/image_transformers.dart @@ -1,8 +1,11 @@ import 'dart:convert'; -import 'package:core/core.dart'; +import 'package:core/data/network/dio_client.dart'; +import 'package:core/data/utils/compress_file_utils.dart'; +import 'package:core/presentation/extensions/html_extension.dart'; import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:html/dom.dart'; @@ -12,35 +15,66 @@ class ImageTransformer extends DomTransformer { const ImageTransformer(); @override - Future process( - Document document, - { - Map? mapUrlDownloadCID, - DioClient? dioClient - } - ) async { - final compressFileUtils = CompressFileUtils(); - final imageElements = document.querySelectorAll('img[src^="cid:"]'); - log('ImageTransformer::process(): imageElements: ${imageElements.length}'); + Future process({ + required Document document, + required DioClient dioClient, + Map? mapUrlDownloadCID, + }) async { + final imageElements = document.querySelectorAll('img'); await Future.wait(imageElements.map((imageElement) async { - imageElement.attributes['style'] = 'display: inline;max-width: 100%;height: auto;'; + var exStyle = imageElement.attributes['style']; + if (exStyle != null) { + if (!exStyle.contains('display')) { + exStyle = '$exStyle display:inline;'; + } + if (!exStyle.contains('max-width')) { + exStyle = '$exStyle max-width:100%;'; + } + imageElement.attributes['style'] = exStyle; + } else { + imageElement.attributes['style'] = 'display:inline;max-width:100%;'; + } final src = imageElement.attributes['src']; - log('ImageTransformer::process(): src: $src'); - final cid = src?.replaceFirst('cid:', '').trim(); - final urlDownloadCid = mapUrlDownloadCID?[cid]; - log('ImageTransformer::process(): urlDownloadCid: $urlDownloadCid'); - if (urlDownloadCid?.isNotEmpty == true && dioClient != null) { - final imgBase64Uri = await loadAsyncNetworkImageToBase64( - dioClient, - compressFileUtils, - urlDownloadCid!); - if (imgBase64Uri.isNotEmpty) { - imageElement.attributes['src'] = imgBase64Uri; + + if (src == null) return; + + if (src.startsWith('cid:') && mapUrlDownloadCID != null) { + final imageBase64 = await _convertCidToBase64Image( + dioClient: dioClient, + mapUrlDownloadCID: mapUrlDownloadCID, + imageSource: src + ); + imageElement.attributes['src'] = imageBase64 ?? src; + } else if (src.startsWith('https://') || src.startsWith('http://')) { + if (!imageElement.attributes.containsKey('loading')) { + imageElement.attributes['loading'] = 'lazy'; } } })); } + Future _convertCidToBase64Image({ + required DioClient dioClient, + required Map mapUrlDownloadCID, + required String imageSource + }) async { + final cid = imageSource.replaceFirst('cid:', '').trim(); + final urlDownloadCid = mapUrlDownloadCID[cid]; + + if (urlDownloadCid == null || urlDownloadCid.isEmpty) return null; + + final compressFileUtils = CompressFileUtils(); + final imgBase64Uri = await loadAsyncNetworkImageToBase64( + dioClient, + compressFileUtils, + urlDownloadCid + ); + + if (imgBase64Uri.isEmpty) return null; + + return imgBase64Uri; + } + Future loadAsyncNetworkImageToBase64( DioClient dioClient, CompressFileUtils compressFileUtils, @@ -52,7 +86,7 @@ class ImageTransformer extends DomTransformer { options: Options(responseType: ResponseType.bytes)); if (responseData != null) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return encodeToBase64Uri(responseData); } else { final bytesCompressed = await compressFileUtils.compressBytesDataImage(responseData); diff --git a/core/lib/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart new file mode 100644 index 0000000000..68e80933be --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart @@ -0,0 +1,35 @@ + +import 'package:core/data/network/dio_client.dart'; +import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; +import 'package:core/presentation/utils/html_transformer/html_template.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:html/dom.dart'; + +class RemoveTooltipLinkTransformer extends DomTransformer { + + const RemoveTooltipLinkTransformer(); + + @override + Future process({ + required Document document, + required DioClient dioClient, + Map? mapUrlDownloadCID, + }) async { + final linkElements = document.querySelectorAll('a.$nameClassToolTip'); + await Future.wait(linkElements.map((linkElement) async { + final classAttribute = linkElement.attributes['class']; + if (classAttribute != null) { + final newClassAttribute = classAttribute.replaceFirst(nameClassToolTip, ''); + linkElement.attributes['class'] = newClassAttribute; + } + final listSpanTag = linkElement.querySelectorAll('span.tooltiptext'); + log('RemoveTooltipLinkTransformer::process:listSpanTag: ${listSpanTag.length}'); + if (listSpanTag.isNotEmpty) { + for (var element in listSpanTag) { + element.remove(); + } + } + })); + } + +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/dom/script_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/script_transformers.dart index 3176f363ec..a021f60b99 100644 --- a/core/lib/presentation/utils/html_transformer/dom/script_transformers.dart +++ b/core/lib/presentation/utils/html_transformer/dom/script_transformers.dart @@ -8,13 +8,11 @@ class RemoveScriptTransformer extends DomTransformer { const RemoveScriptTransformer(); @override - Future process( - Document document, - { - Map? mapUrlDownloadCID, - DioClient? dioClient - } - ) async { + Future process({ + required Document document, + required DioClient dioClient, + Map? mapUrlDownloadCID, + }) async { final scriptElements = document.getElementsByTagName('script'); await Future.wait(scriptElements.map((scriptElement) async { scriptElement.remove(); diff --git a/core/lib/presentation/utils/html_transformer/dom/sigature_transformers.dart b/core/lib/presentation/utils/html_transformer/dom/sigature_transformers.dart index a8466c8616..c82bedf8f1 100644 --- a/core/lib/presentation/utils/html_transformer/dom/sigature_transformers.dart +++ b/core/lib/presentation/utils/html_transformer/dom/sigature_transformers.dart @@ -8,13 +8,11 @@ class SignatureTransformer extends DomTransformer { const SignatureTransformer(); @override - Future process( - Document document, - { - Map? mapUrlDownloadCID, - DioClient? dioClient - } - ) async { + Future process({ + required Document document, + required DioClient dioClient, + Map? mapUrlDownloadCID, + }) async { final signatureElements = document.querySelectorAll('div.tmail-signature'); await Future.wait(signatureElements.map((element) async { element.attributes['class'] = 'tmail-signature-blocked'; diff --git a/core/lib/presentation/utils/html_transformer/html_template.dart b/core/lib/presentation/utils/html_transformer/html_template.dart index d99e9b44c9..44e303de1d 100644 --- a/core/lib/presentation/utils/html_transformer/html_template.dart +++ b/core/lib/presentation/utils/html_transformer/html_template.dart @@ -1,4 +1,6 @@ +import 'package:flutter/material.dart'; + const nameClassToolTip = 'tmail-tooltip'; const tooltipLinkCss = ''' @@ -27,6 +29,7 @@ String generateHtml(String content, { String? styleCSS, String? javaScripts, bool hideScrollBar = true, + TextDirection? direction }) { return ''' @@ -51,37 +54,11 @@ String generateHtml(String content, { ''' : ''} ${styleCSS ?? ''} - ${javaScripts ?? ''} - +
$content
+ ${javaScripts ?? ''} '''; -} - -const bodyCssStyleForEditor = ''' - -'''; \ No newline at end of file +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/html_transform.dart b/core/lib/presentation/utils/html_transformer/html_transform.dart index 1dd8f021ac..ce57dcb042 100644 --- a/core/lib/presentation/utils/html_transformer/html_transform.dart +++ b/core/lib/presentation/utils/html_transformer/html_transform.dart @@ -1,36 +1,36 @@ +import 'dart:convert'; + import 'package:core/core.dart'; import 'package:core/presentation/utils/html_transformer/message_content_transformer.dart'; class HtmlTransform { - final String _contentHtml; - Map? mapUrlDownloadCID; - DioClient? dioClient; + final DioClient _dioClient; + final HtmlEscape _htmlEscape; - HtmlTransform( - this._contentHtml, - { - this.mapUrlDownloadCID, - this.dioClient, - } - ); + HtmlTransform(this._dioClient, this._htmlEscape); /// Transforms this message to HTML code. - Future transformToHtml({TransformConfiguration? transformConfiguration}) async { - transformConfiguration ??= TransformConfiguration.create(); - final transformer = MessageContentTransformer(transformConfiguration); + Future transformToHtml({ + required String htmlContent, + required TransformConfiguration transformConfiguration, + Map? mapCidImageDownloadUrl, + }) async { + final transformer = MessageContentTransformer(transformConfiguration, _dioClient, _htmlEscape); final document = await transformer.toDocument( - _contentHtml, - mapUrlDownloadCID: mapUrlDownloadCID, - dioClient: dioClient); + message: htmlContent, + mapUrlDownloadCID: mapCidImageDownloadUrl + ); return document.outerHtml; } /// Transforms this message to Text Plain. - String transformToTextPlain({TransformConfiguration? transformConfiguration}) { - transformConfiguration ??= TransformConfiguration.create(); - final transformer = MessageContentTransformer(transformConfiguration); - final message = transformer.toMessage(_contentHtml); + String transformToTextPlain({ + required String content, + required TransformConfiguration transformConfiguration + }) { + final transformer = MessageContentTransformer(transformConfiguration, _dioClient, _htmlEscape); + final message = transformer.toMessage(content); return message; } } \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/html_utils.dart b/core/lib/presentation/utils/html_transformer/html_utils.dart index c187bf6ea8..7ae5eec1a5 100644 --- a/core/lib/presentation/utils/html_transformer/html_utils.dart +++ b/core/lib/presentation/utils/html_transformer/html_utils.dart @@ -1,5 +1,7 @@ import 'package:core/presentation/utils/html_transformer/html_event_action.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; class HtmlUtils { @@ -41,22 +43,15 @@ class HtmlUtils { let maxOffset = Math.round(scrollWidth - offsetWidth); let scrollLeftRounded = Math.round(newScrollLeft); - /* - console.log('newScrollLeft: ' + newScrollLeft); - console.log('scrollWidth: ' + scrollWidth); - console.log('offsetWidth: ' + offsetWidth); - console.log('maxOffset: ' + maxOffset); - console.log('scrollLeftRounded: ' + scrollLeftRounded); */ - if (xDiff > 0) { if (maxOffset === scrollLeftRounded || maxOffset === (scrollLeftRounded + 1) || maxOffset === (scrollLeftRounded - 1)) { - window.$scrollEventJSChannelName.postMessage('${HtmlEventAction.scrollRightEndAction}'); + window.flutter_inappwebview.callHandler('$scrollEventJSChannelName', '${HtmlEventAction.scrollRightEndAction}'); } } else { if (scrollLeftRounded === 0) { - window.$scrollEventJSChannelName.postMessage('${HtmlEventAction.scrollLeftEndAction}'); + window.flutter_inappwebview.callHandler('$scrollEventJSChannelName', '${HtmlEventAction.scrollLeftEndAction}'); } } } @@ -65,4 +60,32 @@ class HtmlUtils { yDown = null; } '''; + + static String customCssStyleHtmlEditor({TextDirection direction = TextDirection.ltr}) { + if (PlatformInfo.isWeb) { + return ''' + + '''; + } else if (PlatformInfo.isMobile) { + return ''' + #editor { + direction: ${direction.name}; + } + + #editor .tmail-signature { + text-align: ${direction == TextDirection.rtl ? 'right' : 'left'}; + } + '''; + } else { + return ''; + } + } } diff --git a/core/lib/presentation/utils/html_transformer/message_content_transformer.dart b/core/lib/presentation/utils/html_transformer/message_content_transformer.dart index 761e4f2afe..034855b738 100644 --- a/core/lib/presentation/utils/html_transformer/message_content_transformer.dart +++ b/core/lib/presentation/utils/html_transformer/message_content_transformer.dart @@ -1,50 +1,55 @@ -import 'package:core/core.dart'; +import 'dart:convert'; + +import 'package:core/data/network/dio_client.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:html/dom.dart'; import 'package:html/parser.dart' show parse; /// Transforms messages class MessageContentTransformer { - /// The configuration used for the transformation - final TransformConfiguration configuration; + /// The _configuration used for the transformation + final TransformConfiguration _configuration; + final DioClient _dioClient; + final HtmlEscape _htmlEscape; - MessageContentTransformer(this.configuration); + MessageContentTransformer( + this._configuration, + this._dioClient, + this._htmlEscape + ); - Future _transformDocument( - Document document, - { - Map? mapUrlDownloadCID, - DioClient? dioClient - } - ) async { + Future _transformDocument({ + required Document document, + Map? mapUrlDownloadCID + }) async { await Future.wait([ - if (configuration.domTransformers.isNotEmpty) - ...configuration.domTransformers.map((domTransformer) async => + if (_configuration.domTransformers.isNotEmpty) + ..._configuration.domTransformers.map((domTransformer) async => domTransformer.process( - document, - mapUrlDownloadCID: mapUrlDownloadCID, - dioClient: dioClient)) + document: document, + dioClient: _dioClient, + mapUrlDownloadCID: mapUrlDownloadCID, + ) + ) ]); } - Future toDocument( - String message, - { - Map? mapUrlDownloadCID, - DioClient? dioClient - } - ) async { + Future toDocument({ + required String message, + Map? mapUrlDownloadCID + }) async { final document = parse(message); await _transformDocument( - document, - mapUrlDownloadCID: mapUrlDownloadCID, - dioClient: dioClient); + document: document, + mapUrlDownloadCID: mapUrlDownloadCID, + ); return document; } String _transformMessage(String message) { - if (configuration.textTransformers.isNotEmpty) { - for (var transformer in configuration.textTransformers) { - message = transformer.process(message); + if (_configuration.textTransformers.isNotEmpty) { + for (var transformer in _configuration.textTransformers) { + message = transformer.process(message, _htmlEscape); } } return message; diff --git a/core/lib/presentation/utils/html_transformer/sanitize_autolink_filter.dart b/core/lib/presentation/utils/html_transformer/sanitize_autolink_filter.dart new file mode 100644 index 0000000000..86b2712db3 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/sanitize_autolink_filter.dart @@ -0,0 +1,65 @@ + +import 'dart:convert'; + +import 'package:core/utils/app_logger.dart'; +import 'package:linkify/linkify.dart'; + +class SanitizeAutolinkFilter { + + final HtmlEscape htmlEscape; + final _linkifyOption = const LinkifyOptions( + humanize: true, + looseUrl: true, + defaultToHttps: true, + removeWww: true + ); + final _linkifier = [ + const EmailLinkifier(), + const UrlLinkifier() + ]; + + SanitizeAutolinkFilter(this.htmlEscape); + + String process(String inputText) { + if (inputText.isEmpty) { + return ''; + } + + final elements = linkify( + inputText, + options: _linkifyOption, + linkifiers: _linkifier + ); + log('AutolinkFilter::process:elements: $elements'); + final htmlTextBuffer = StringBuffer(); + + for (var element in elements) { + if (element is TextElement) { + final escapedHtml = htmlEscape.convert(element.text); + htmlTextBuffer.write(escapedHtml); + } else if (element is EmailElement) { + final emailLinkTag = _buildEmailLinkTag( + mailToLink: element.url, + value: element.text + ); + htmlTextBuffer.write(emailLinkTag); + } else if (element is UrlElement) { + final urlLinkTag = _buildUrlLinkTag( + urlLink: element.url, + value: element.text + ); + htmlTextBuffer.write(urlLinkTag); + } + } + + return htmlTextBuffer.toString(); + } + + String _buildUrlLinkTag({required String urlLink, required String value}) { + return '$value'; + } + + String _buildEmailLinkTag({required String mailToLink, required String value}) { + return '$value'; + } +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/text/convert_url_string_to_html_links_transformers.dart b/core/lib/presentation/utils/html_transformer/text/convert_url_string_to_html_links_transformers.dart deleted file mode 100644 index 5f9f3d0cb1..0000000000 --- a/core/lib/presentation/utils/html_transformer/text/convert_url_string_to_html_links_transformers.dart +++ /dev/null @@ -1,14 +0,0 @@ - -import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart'; -import 'package:core/utils/linkify_html.dart'; - -class ConvertUrlStringToHtmlLinksTransformers extends TextTransformer { - - const ConvertUrlStringToHtmlLinksTransformers(); - - @override - String process(String text) { - final texValid = LinkifyHtml().generateLinkify(text); - return texValid; - } -} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart b/core/lib/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart new file mode 100644 index 0000000000..0bab1f3557 --- /dev/null +++ b/core/lib/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart @@ -0,0 +1,13 @@ + +import 'dart:convert'; + +import 'package:core/presentation/utils/html_transformer/sanitize_autolink_filter.dart'; +import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart'; + +class SanitizeAutolinkHtmlTransformers extends TextTransformer { + + const SanitizeAutolinkHtmlTransformers(); + + @override + String process(String text, HtmlEscape htmlEscape) => SanitizeAutolinkFilter(htmlEscape).process(text); +} \ No newline at end of file diff --git a/core/lib/presentation/utils/html_transformer/transform_configuration.dart b/core/lib/presentation/utils/html_transformer/transform_configuration.dart index 228cb20653..f581e3d4c0 100644 --- a/core/lib/presentation/utils/html_transformer/transform_configuration.dart +++ b/core/lib/presentation/utils/html_transformer/transform_configuration.dart @@ -1,12 +1,15 @@ +import 'package:core/core.dart'; import 'package:core/presentation/utils/html_transformer/base/dom_transformer.dart'; import 'package:core/presentation/utils/html_transformer/base/text_transformer.dart'; import 'package:core/presentation/utils/html_transformer/dom/add_target_blank_in_tag_a_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/blockcode_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/blockquoted_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart'; +import 'package:core/presentation/utils/html_transformer/dom/remove_tooltip_link_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart'; import 'package:core/presentation/utils/html_transformer/dom/sigature_transformers.dart'; +import 'package:core/presentation/utils/html_transformer/text/sanitize_autolink_html_transformers.dart'; /// Contains the configuration for all transformations. class TransformConfiguration { @@ -25,6 +28,31 @@ class TransformConfiguration { this.textTransformers ); + factory TransformConfiguration.fromDomTransformers(List domTransformers) => TransformConfiguration(domTransformers, []); + + factory TransformConfiguration.empty() => const TransformConfiguration([], []); + + factory TransformConfiguration.forReplyForwardEmail() => TransformConfiguration.fromDomTransformers([ + if (PlatformInfo.isWeb) + const RemoveTooltipLinkTransformer(), + const SignatureTransformer(), + ]); + + factory TransformConfiguration.forDraftsEmail() => TransformConfiguration.empty(); + + factory TransformConfiguration.forPreviewEmailOnWeb() => TransformConfiguration.create( + customDomTransformers: [ + const RemoveScriptTransformer(), + const BlockQuotedTransformer(), + const BlockCodeTransformer(), + const AddTargetBlankInTagATransformer(), + const ImageTransformer(), + const AddTooltipLinkTransformer(), + ] + ); + + factory TransformConfiguration.forPreviewEmail() => TransformConfiguration.standardConfiguration; + /// Provides easy access to a standard configuration that does not block external images. static const TransformConfiguration standardConfiguration = TransformConfiguration( standardDomTransformers, @@ -39,11 +67,13 @@ class TransformConfiguration { List? customTextTransformers }) { final domTransformers = (customDomTransformers != null && customDomTransformers.isNotEmpty) - ? [...customDomTransformers] - : [...standardDomTransformers]; + ? customDomTransformers + : standardDomTransformers; + final textTransformers = (customTextTransformers != null && customTextTransformers.isNotEmpty) - ? [...customTextTransformers] - : standardTextTransformers; + ? customTextTransformers + : standardTextTransformers; + return TransformConfiguration( domTransformers, textTransformers @@ -52,12 +82,13 @@ class TransformConfiguration { static const List standardDomTransformers = [ RemoveScriptTransformer(), - SignatureTransformer(), BlockQuotedTransformer(), BlockCodeTransformer(), AddTargetBlankInTagATransformer(), ImageTransformer(), ]; - static const List standardTextTransformers = []; + static const List standardTextTransformers = [ + SanitizeAutolinkHtmlTransformers() + ]; } \ No newline at end of file diff --git a/core/lib/presentation/utils/icon_utils.dart b/core/lib/presentation/utils/icon_utils.dart index a00533bfa7..49ea41ccd4 100644 --- a/core/lib/presentation/utils/icon_utils.dart +++ b/core/lib/presentation/utils/icon_utils.dart @@ -1,6 +1,6 @@ -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; class IconUtils { - static const double defaultIconSize = BuildUtils.isWeb ? 20.0 : 24.0; + static const double defaultIconSize = PlatformInfo.isWeb ? 20.0 : 24.0; } \ No newline at end of file diff --git a/core/lib/presentation/utils/keyboard_utils.dart b/core/lib/presentation/utils/keyboard_utils.dart index a1f306bb95..a6a3fa65c8 100644 --- a/core/lib/presentation/utils/keyboard_utils.dart +++ b/core/lib/presentation/utils/keyboard_utils.dart @@ -1,10 +1,12 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; class KeyboardUtils { - void hideKeyboard(BuildContext context) { - FocusScopeNode currentFocus = FocusScope.of(context); - if (!currentFocus.hasPrimaryFocus) { - currentFocus.unfocus(); - } + static void hideKeyboard(BuildContext context) { + FocusScope.of(context).unfocus(); + } + + static void hideSystemKeyboardMobile() { + SystemChannels.textInput.invokeMethod('TextInput.hide'); } } \ No newline at end of file diff --git a/core/lib/presentation/utils/responsive_utils.dart b/core/lib/presentation/utils/responsive_utils.dart index 07da77c2ed..ed91ad75e6 100644 --- a/core/lib/presentation/utils/responsive_utils.dart +++ b/core/lib/presentation/utils/responsive_utils.dart @@ -1,4 +1,4 @@ -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; @@ -8,20 +8,20 @@ class ResponsiveUtils { static const double defaultSizeDrawer = 320; static const double defaultSizeMenu = 256; - final int heightShortest = 600; + static const int heightShortest = 600; - final int minDesktopWidth = 1200; - final int minTabletWidth = 600; - final int minTabletLargeWidth = 900; + static const int minDesktopWidth = 1200; + static const int minTabletWidth = 600; + static const int minTabletLargeWidth = 900; - final double _loginTextFieldWidthSmallScreen = 280.0; - final double _loginTextFieldWidthLargeScreen = 320.0; - final double _loginButtonWidth = 240.0; + static const double _loginTextFieldWidthSmallScreen = 280.0; + static const double _loginTextFieldWidthLargeScreen = 320.0; + static const double _loginButtonWidth = 240.0; - final double tabletHorizontalMargin = 120.0; - final double tabletVerticalMargin = 200.0; - final double desktopVerticalMargin = 120.0; - final double desktopHorizontalMargin = 200.0; + static const double tabletHorizontalMargin = 120.0; + static const double tabletVerticalMargin = 200.0; + static const double desktopVerticalMargin = 120.0; + static const double desktopHorizontalMargin = 200.0; bool isScreenWithShortestSide(BuildContext context) => context.mediaQueryShortestSide < minTabletWidth; @@ -77,12 +77,12 @@ class ResponsiveUtils { if (isPortraitMobile(context)) { return widthScreen; } else { - return widthScreen < 444 ? widthScreen : 444; + return widthScreen < 424 ? widthScreen : 424; } } bool hasLeftMenuDrawerActive(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return isMobile(context) || isTablet(context) || isTabletLarge(context); @@ -92,13 +92,13 @@ class ResponsiveUtils { } bool isWebDesktop(BuildContext context) => - BuildUtils.isWeb && isDesktop(context); + PlatformInfo.isWeb && isDesktop(context); bool isWebNotDesktop(BuildContext context) => - BuildUtils.isWeb && !isDesktop(context); + PlatformInfo.isWeb && !isDesktop(context); bool mailboxDashboardOnlyHasEmailView(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return isMobile(context) || isTablet(context); } else { return isPortraitMobile(context) || @@ -108,7 +108,7 @@ class ResponsiveUtils { } bool landscapeTabletSupported(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return isTabletLarge(context); } else { return !isLandscapeMobile(context) && (isLandscapeTablet(context) || @@ -116,4 +116,6 @@ class ResponsiveUtils { isDesktop(context)); } } + + static bool isMatchedMobileWidth(double width) => width < minTabletWidth; } \ No newline at end of file diff --git a/core/lib/presentation/utils/style_utils.dart b/core/lib/presentation/utils/style_utils.dart index 05722cc9c4..8cc8cf1fda 100644 --- a/core/lib/presentation/utils/style_utils.dart +++ b/core/lib/presentation/utils/style_utils.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; class CommonTextStyle { @@ -9,9 +10,9 @@ class CommonTextStyle { fontWeight: FontWeight.normal, ); - static const defaultTextOverFlow = BuildUtils.isWeb + static const defaultTextOverFlow = PlatformInfo.isWeb ? TextOverflow.fade : TextOverflow.ellipsis; - static const defaultSoftWrap = BuildUtils.isWeb ? false : true; + static const defaultSoftWrap = PlatformInfo.isWeb ? false : true; } \ No newline at end of file diff --git a/core/lib/presentation/utils/theme_utils.dart b/core/lib/presentation/utils/theme_utils.dart index 843d1d14aa..f02385c6e6 100644 --- a/core/lib/presentation/utils/theme_utils.dart +++ b/core/lib/presentation/utils/theme_utils.dart @@ -21,8 +21,8 @@ class ThemeUtils { static TextTheme get _textTheme { return const TextTheme( - bodyText1: TextStyle(color: AppColor.baseTextColor), - bodyText2: TextStyle(color: AppColor.baseTextColor), + bodyMedium: TextStyle(color: AppColor.baseTextColor), + bodySmall: TextStyle(color: AppColor.baseTextColor), ); } diff --git a/lib/features/contact/presentation/widgets/gradient_color_avatar_icon.dart b/core/lib/presentation/views/avatar/gradient_circle_avatar_icon.dart similarity index 67% rename from lib/features/contact/presentation/widgets/gradient_color_avatar_icon.dart rename to core/lib/presentation/views/avatar/gradient_circle_avatar_icon.dart index 14a3d52637..3873384d0a 100644 --- a/lib/features/contact/presentation/widgets/gradient_color_avatar_icon.dart +++ b/core/lib/presentation/views/avatar/gradient_circle_avatar_icon.dart @@ -2,18 +2,21 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:flutter/material.dart'; -class GradientColorAvatarIcon extends StatelessWidget { +class GradientCircleAvatarIcon extends StatelessWidget { final List colors; final double iconSize; final double labelFontSize; final String label; + final TextStyle? textStyle; - const GradientColorAvatarIcon(this.colors, { + const GradientCircleAvatarIcon({ Key? key, + required this.colors, this.iconSize = 40, this.label = '', this.labelFontSize = 24.0, + this.textStyle, }) : super(key: key); @override @@ -23,21 +26,22 @@ class GradientColorAvatarIcon extends StatelessWidget { height: iconSize, alignment: Alignment.center, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(iconSize * 0.5), - border: Border.all(color: Colors.transparent), + shape: BoxShape.circle, gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: const [0.0, 1.0], - colors: colors), + colors: colors + ), color: AppColor.avatarColor ), - child: Text( - label, - style: TextStyle( + child: DefaultTextStyle( + style: textStyle ?? TextStyle( color: Colors.white, fontSize: labelFontSize, - fontWeight: FontWeight.w600) + fontWeight: FontWeight.w600 + ), + child: Text(label), ) ); } diff --git a/core/lib/presentation/views/background/background_widget_builder.dart b/core/lib/presentation/views/background/background_widget_builder.dart deleted file mode 100644 index 47bd84fb8c..0000000000 --- a/core/lib/presentation/views/background/background_widget_builder.dart +++ /dev/null @@ -1,89 +0,0 @@ - -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/presentation/views/responsive/responsive_widget.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -class BackgroundWidgetBuilder extends StatelessWidget { - - final ResponsiveUtils responsiveUtils; - final String title; - final String? iconSVG; - final String? subTitle; - final double? maxWidth; - - const BackgroundWidgetBuilder( - this.title, - this.responsiveUtils, { - Key? key, - this.iconSVG, - this.subTitle, - this.maxWidth - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Center( - key: const Key('background_widget'), - child: SizedBox( - width: maxWidth ?? 360, - child: ResponsiveWidget( - mobile: CustomScrollView(slivers: [ - SliverFillRemaining(child: _buildMessageBody(context)) - ]), - landscapeMobile: SingleChildScrollView(child: _buildMessageBody(context)), - responsiveUtils: responsiveUtils, - ), - ) - ); - } - - Widget _buildMessageBody(BuildContext context) { - return Container( - color: Colors.transparent, - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Column( - mainAxisAlignment: responsiveUtils.isLandscapeMobile(context) - ? MainAxisAlignment.start - : MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (iconSVG != null) - SvgPicture.asset( - iconSVG!, - width: 212, - height: 212, - fit: BoxFit.fill - ), - Padding( - padding: EdgeInsets.only(top: iconSVG != null ? 12 : 0), - child: Text( - title, - style: const TextStyle( - color: Colors.black, - fontSize: 20, - fontWeight: FontWeight.normal - ), - textAlign: TextAlign.center, - ), - ), - if (subTitle != null) - Padding( - padding: const EdgeInsets.only(top: 12), - child: Text( - subTitle!, - style: const TextStyle( - color: AppColor.colorSubtitle, - fontSize: 15, - fontWeight: FontWeight.normal - ), - textAlign: TextAlign.center, - ), - ) - ], - ), - height: MediaQuery.of(context).size.height, - ); - } -} \ No newline at end of file diff --git a/core/lib/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart b/core/lib/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart index ed64e18fce..75c4541c00 100644 --- a/core/lib/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart +++ b/core/lib/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart @@ -1,5 +1,6 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; @@ -19,8 +20,9 @@ class ConfirmationDialogActionSheetBuilder { TextStyle? _styleConfirmButton; TextStyle? _styleCancelButton; TextStyle? _styleMessage; + List? listTextSpan; - ConfirmationDialogActionSheetBuilder(this._context); + ConfirmationDialogActionSheetBuilder(this._context, {this.listTextSpan}); void onConfirmAction(String confirmText, OnConfirmActionClick onConfirmActionClick) { _onConfirmActionClick = onConfirmActionClick; @@ -48,30 +50,46 @@ class ConfirmationDialogActionSheetBuilder { _styleCancelButton = style; } - void show() { - showCupertinoModalPopup( + void show() async { + await showCupertinoModalPopup( context: _context, barrierColor: AppColor.colorDefaultCupertinoActionSheet, builder: (context) => PointerInterceptor(child: CupertinoActionSheet( actions: [ - Container( + if (_messageText != null && _messageText!.isNotEmpty) + Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + color: Colors.white, + child: MouseRegion( + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, + child: CupertinoActionSheetAction( + child: Text( + _messageText ?? '', + textAlign: TextAlign.center, + style: _styleMessage ?? const TextStyle(fontSize: 14, color: AppColor.colorMessageConfirmDialog)), + onPressed: () => {}, + ), + ) + ) + else if (listTextSpan != null) + Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), color: Colors.white, child: MouseRegion( - cursor: BuildUtils.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( - child: Text( - _messageText ?? '', - textAlign: TextAlign.center, - style: _styleMessage ?? const TextStyle(fontSize: 14, color: AppColor.colorMessageConfirmDialog)), + child: RichText(text: TextSpan( + style: _styleMessage ?? const TextStyle(fontSize: 14, color: AppColor.colorMessageConfirmDialog), + children: listTextSpan + )), onPressed: () => {}, ), ) - ), + ), Container( color: Colors.white, child: MouseRegion( - cursor: BuildUtils.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( child: Text( _confirmText ?? '', @@ -82,7 +100,7 @@ class ConfirmationDialogActionSheetBuilder { ), ], cancelButton: MouseRegion( - cursor: BuildUtils.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( child: Text( _cancelText ?? '', diff --git a/core/lib/presentation/views/button/button_builder.dart b/core/lib/presentation/views/button/button_builder.dart deleted file mode 100644 index 6758b30d21..0000000000 --- a/core/lib/presentation/views/button/button_builder.dart +++ /dev/null @@ -1,169 +0,0 @@ - -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -typedef OnPressActionClick = void Function(); -typedef OnPressActionWithPositionClick = void Function(RelativeRect? position); - -class ButtonBuilder { - OnPressActionClick? _onPressActionClick; - OnPressActionWithPositionClick? _onPressActionWithPositionClick; - - BuildContext? _context; - final String? _icon; - String? _text; - double? _size; - EdgeInsets? _paddingIcon; - bool? _isVertical; - Key? _key; - Color? _iconColor; - Color? _colorButton; - TextStyle? _textStyle; - BoxDecoration? _decoration; - Widget? _iconAction; - double? _radiusSplash; - double? _maxWidth; - EdgeInsets? _padding; - - void key(Key key) { - _key = key; - } - - void context(BuildContext context) { - _context = context; - } - - void size(double size) { - _size = size; - } - - void maxWidth(double? size) { - _maxWidth = size; - } - - void iconColor(Color color) { - _iconColor = color; - } - - void colorButton(Color color) { - _colorButton = color; - } - - void decoration(BoxDecoration decoration) { - _decoration = decoration; - } - - void addIconAction(Widget icon) { - _iconAction = icon; - } - - void radiusSplash(double? radius) { - _radiusSplash = radius; - } - - void padding(EdgeInsets? padding) { - _padding = padding; - } - - void textStyle(TextStyle style) { - _textStyle = style; - } - - void paddingIcon(EdgeInsets paddingIcon) { - _paddingIcon = paddingIcon; - } - - void text(String? text, {required bool isVertical}) { - _text = text; - _isVertical = isVertical; - } - - ButtonBuilder(this._icon); - - void onPressActionClick(OnPressActionClick onPressActionClick) { - _onPressActionClick = onPressActionClick; - } - - void addOnPressActionWithPositionClick(OnPressActionWithPositionClick onPressActionClick) { - _onPressActionWithPositionClick = onPressActionClick; - } - - Widget build() { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () => _onPressActionClick != null ? _onPressActionClick!.call() : null, - onTapDown: (detail) { - if (_onPressActionWithPositionClick != null && _context != null) { - final screenSize = MediaQuery.of(_context!).size; - final offset = detail.globalPosition; - final position = RelativeRect.fromLTRB( - offset.dx, - offset.dy, - screenSize.width - offset.dx, - screenSize.height - offset.dy, - ); - _onPressActionWithPositionClick?.call(position); - } - }, - customBorder: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(_radiusSplash ?? 20)), - child: Container( - key: _key, - alignment: Alignment.center, - color: _decoration == null ? _colorButton : null, - decoration: _decoration, - width: _maxWidth, - padding: _padding ?? EdgeInsets.zero, - child: _buildBody() - ) - ), - ); - } - - Widget _buildBody() { - if (_text != null) { - return _isVertical! - ? Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildIcon(), - _buildText(), - if (_iconAction != null) _iconAction! - ]) - : Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildIcon(), - _buildText(), - if (_iconAction != null) _iconAction! - ]); - } else { - return _buildIcon(); - } - } - - Widget _buildIcon() => Padding( - padding: _paddingIcon ?? const EdgeInsets.all(10), - child: SvgPicture.asset( - _icon ?? '', - width: _size ?? 24, - height: _size ?? 24, - fit: BoxFit.fill, - color: _iconColor)); - - Widget _buildText() { - return Text( - _text ?? '', - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: _textStyle ?? const TextStyle( - fontSize: 12, - color: AppColor.colorTextButton), - ); - } -} \ No newline at end of file diff --git a/core/lib/presentation/views/button/icon_button_web.dart b/core/lib/presentation/views/button/icon_button_web.dart index 826844468c..fd053155ad 100644 --- a/core/lib/presentation/views/button/icon_button_web.dart +++ b/core/lib/presentation/views/button/icon_button_web.dart @@ -6,13 +6,13 @@ import 'package:flutter_svg/flutter_svg.dart'; typedef IconWebCallback = void Function(); typedef IconWebHasPositionCallback = void Function(RelativeRect); typedef OnTapIconButtonCallbackAction = void Function(); -typedef OnTapDownIconButtonCallbackAction = void Function(TapDownDetails TapDetails); +typedef OnTapDownIconButtonCallbackAction = void Function(TapDownDetails tapDetails); Widget buildIconWeb({ required Widget icon, String? tooltip, IconWebCallback? onTap, - EdgeInsets? iconPadding, + EdgeInsetsGeometry? iconPadding, double? iconSize, double? splashRadius, double? minSize, @@ -38,7 +38,7 @@ Widget buildIconWeb({ Widget buildSVGIconButton({ required String icon, String? tooltip, - EdgeInsets? padding, + EdgeInsetsGeometry? padding, double? iconSize, Color? iconColor, OnTapIconButtonCallbackAction? onTap, @@ -51,7 +51,7 @@ Widget buildSVGIconButton({ width: iconSize, height: iconSize, fit: BoxFit.fill, - color: iconColor, + colorFilter: iconColor.asFilter(), ), ); @@ -99,30 +99,9 @@ Widget buildIconWebHasPosition(BuildContext context, { ); } -Widget buildTextCircleButton(String text, { - TextStyle? textStyle, - IconWebCallback? onTap, -}) { - return Material( - shape: const CircleBorder(), - color: Colors.transparent, - child: TextButton( - child: Text( - text, - style: textStyle ?? const TextStyle(fontWeight: FontWeight.normal, fontSize: 15, color: AppColor.lineItemListColor)), - style: ButtonStyle( - overlayColor: MaterialStateProperty.resolveWith((Set states) => AppColor.colorFocusButton), - shape: MaterialStateProperty.all(const CircleBorder()), - padding: MaterialStateProperty.resolveWith((Set states) => EdgeInsets.zero), - elevation: MaterialStateProperty.resolveWith((Set states) => 0)), - onPressed: () => onTap?.call() - ) - ); -} - Widget buildTextIcon(String text, { TextStyle? textStyle, - EdgeInsets? padding, + EdgeInsetsGeometry? padding, IconWebCallback? onTap, }) { return Material( @@ -142,7 +121,7 @@ Widget buildTextButton(String text, { double? width, double? height, Color? backgroundColor, - EdgeInsets? padding, + EdgeInsetsGeometry? padding, double? radius, IconWebCallback? onTap, FocusNode? focusNode, @@ -155,7 +134,7 @@ Widget buildTextButton(String text, { style: ButtonStyle( backgroundColor: MaterialStateProperty.resolveWith((states) => backgroundColor ?? AppColor.colorTextButton), elevation: MaterialStateProperty.resolveWith((states) => 0), - padding: MaterialStateProperty.resolveWith( + padding: MaterialStateProperty.resolveWith( (Set states) => padding ?? const EdgeInsets.symmetric(horizontal: 8)), shape: MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(radius ?? 0)))), child: Text( @@ -177,7 +156,7 @@ Widget buildButtonWrapText(String name, { double? radius, double? height, double? minWidth, - EdgeInsets? padding, + EdgeInsetsGeometry? padding, FocusNode? focusNode, IconWebCallback? onTap }) { diff --git a/core/lib/presentation/views/button/tmail_button_widget.dart b/core/lib/presentation/views/button/tmail_button_widget.dart new file mode 100644 index 0000000000..3b416a3a6e --- /dev/null +++ b/core/lib/presentation/views/button/tmail_button_widget.dart @@ -0,0 +1,340 @@ + +import 'package:core/presentation/action/action_callback_define.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/container/tmail_container_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class TMailButtonWidget extends StatelessWidget { + + final OnTapActionCallback? onTapActionCallback; + final OnTapActionAtPositionCallback? onTapActionAtPositionCallback; + + final double borderRadius; + final double? width; + final double maxWidth; + final double maxHeight; + final double minWidth; + final String? tooltipMessage; + final Color? backgroundColor; + final EdgeInsetsGeometry? padding; + final EdgeInsetsGeometry? margin; + final String text; + final String? icon; + final bool verticalDirection; + final double? iconSize; + final double iconSpace; + final Color? iconColor; + final TextStyle? textStyle; + final String? trailingIcon; + final double? trailingIconSize; + final Color? trailingIconColor; + final List? boxShadow; + final TextAlign? textAlign; + final bool flexibleText; + final BoxBorder? border; + final TextDirection iconAlignment; + final int? maxLines; + final MainAxisSize mainAxisSize; + + const TMailButtonWidget({ + super.key, + required this.text, + this.onTapActionCallback, + this.onTapActionAtPositionCallback, + this.borderRadius = 20, + this.width, + this.maxWidth = double.infinity, + this.maxHeight = double.infinity, + this.minWidth = 0, + this.tooltipMessage, + this.backgroundColor, + this.padding, + this.verticalDirection = false, + this.icon, + this.iconSize, + this.iconColor, + this.textStyle, + this.iconSpace = 8, + this.trailingIcon, + this.trailingIconSize, + this.trailingIconColor, + this.boxShadow, + this.margin, + this.textAlign, + this.flexibleText = false, + this.border, + this.iconAlignment = TextDirection.ltr, + this.maxLines, + this.mainAxisSize = MainAxisSize.max, + }); + + factory TMailButtonWidget.fromIcon({ + required String icon, + final Key? key, + OnTapActionCallback? onTapActionCallback, + OnTapActionAtPositionCallback? onTapActionAtPositionCallback, + double borderRadius = 20, + double? width, + double maxWidth = double.infinity, + double maxHeight = double.infinity, + double minWidth = 0, + String? tooltipMessage, + Color? backgroundColor, + EdgeInsetsGeometry? padding, + double? iconSize, + Color? iconColor, + double iconSpace = 8, + String? trailingIcon, + double? trailingIconSize, + Color? trailingIconColor, + List? boxShadow, + EdgeInsetsGeometry? margin, + }) { + return TMailButtonWidget( + key: key, + text: '', + onTapActionCallback: onTapActionCallback, + onTapActionAtPositionCallback: onTapActionAtPositionCallback, + borderRadius: borderRadius, + width: width, + maxWidth : maxWidth, + maxHeight: maxHeight, + minWidth: minWidth, + tooltipMessage: tooltipMessage, + backgroundColor: backgroundColor, + padding: padding, + icon: icon, + iconSize: iconSize, + iconColor: iconColor, + iconSpace: iconSpace, + trailingIcon: trailingIcon, + trailingIconSize: trailingIconSize, + trailingIconColor: trailingIconColor, + boxShadow: boxShadow, + margin: margin, + ); + } + + factory TMailButtonWidget.fromText({ + required String text, + final Key? key, + OnTapActionCallback? onTapActionCallback, + OnTapActionAtPositionCallback? onTapActionAtPositionCallback, + double borderRadius = 20, + double? width, + double maxWidth = double.infinity, + double maxHeight = double.infinity, + double minWidth = 0, + String? tooltipMessage, + Color? backgroundColor, + EdgeInsetsGeometry? padding, + TextStyle? textStyle, + List? boxShadow, + EdgeInsetsGeometry? margin, + TextAlign? textAlign, + bool flexibleText = false, + BoxBorder? border, + int? maxLines, + }) { + return TMailButtonWidget( + key: key, + text: text, + onTapActionCallback: onTapActionCallback, + onTapActionAtPositionCallback: onTapActionAtPositionCallback, + borderRadius: borderRadius, + width: width, + maxWidth : maxWidth, + maxHeight: maxHeight, + minWidth: minWidth, + tooltipMessage: tooltipMessage, + backgroundColor: backgroundColor, + padding: padding, + textStyle: textStyle, + boxShadow: boxShadow, + margin: margin, + textAlign: textAlign, + flexibleText: flexibleText, + border: border, + maxLines: maxLines, + ); + } + + @override + Widget build(BuildContext context) { + Widget childWidget; + + if (icon != null && text.isNotEmpty) { + if (verticalDirection) { + childWidget = Column( + mainAxisSize: mainAxisSize, + children: [ + SvgPicture.asset( + icon!, + width: iconSize, + height: iconSize, + fit: BoxFit.fill, + colorFilter: iconColor?.asFilter() + ), + SizedBox(height: iconSpace), + Text( + text, + textAlign: textAlign, + style: textStyle ?? const TextStyle( + fontSize: 12, + color: AppColor.colorTextButtonHeaderThread + ), + maxLines: maxLines, + overflow: maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null, + softWrap: maxLines == 1 ? CommonTextStyle.defaultSoftWrap : null, + ), + if (trailingIcon != null) + Padding( + padding: EdgeInsetsDirectional.only(top: iconSpace), + child: SvgPicture.asset( + trailingIcon!, + width: trailingIconSize, + height: trailingIconSize, + fit: BoxFit.fill, + colorFilter: trailingIconColor?.asFilter() + ), + ), + ] + ); + } else { + if (iconAlignment == TextDirection.ltr) { + childWidget = Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: mainAxisSize, + children: [ + SvgPicture.asset( + icon!, + width: iconSize, + height: iconSize, + fit: BoxFit.fill, + colorFilter: iconColor?.asFilter() + ), + SizedBox(width: iconSpace), + if (flexibleText) + Flexible( + child: Text( + text, + textAlign: textAlign, + style: textStyle ?? const TextStyle( + fontSize: 12, + color: AppColor.colorTextButtonHeaderThread + ), + maxLines: maxLines, + overflow: maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null, + softWrap: maxLines == 1 ? CommonTextStyle.defaultSoftWrap : null, + ), + ) + else + Text( + text, + textAlign: textAlign, + style: textStyle ?? const TextStyle( + fontSize: 12, + color: AppColor.colorTextButtonHeaderThread + ), + maxLines: maxLines, + overflow: maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null, + softWrap: maxLines == 1 ? CommonTextStyle.defaultSoftWrap : null, + ), + if (trailingIcon != null) + Padding( + padding: EdgeInsetsDirectional.only(start: iconSpace), + child: SvgPicture.asset( + trailingIcon!, + width: trailingIconSize, + height: trailingIconSize, + fit: BoxFit.fill, + colorFilter: trailingIconColor?.asFilter() + ), + ), + ] + ); + } else { + childWidget = Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: mainAxisSize, + children: [ + if (flexibleText) + Flexible( + child: Text( + text, + textAlign: textAlign, + style: textStyle ?? const TextStyle( + fontSize: 12, + color: AppColor.colorTextButtonHeaderThread + ), + maxLines: maxLines, + overflow: maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null, + softWrap: maxLines == 1 ? CommonTextStyle.defaultSoftWrap : null, + ), + ) + else + Text( + text, + textAlign: textAlign, + style: textStyle ?? const TextStyle( + fontSize: 12, + color: AppColor.colorTextButtonHeaderThread + ), + maxLines: maxLines, + overflow: maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null, + softWrap: maxLines == 1 ? CommonTextStyle.defaultSoftWrap : null, + ), + SizedBox(width: iconSpace), + SvgPicture.asset( + icon!, + width: iconSize, + height: iconSize, + fit: BoxFit.fill, + colorFilter: iconColor?.asFilter() + ), + ] + ); + } + } + } else if (icon != null) { + childWidget = SvgPicture.asset( + icon!, + width: iconSize, + height: iconSize, + fit: BoxFit.fill, + colorFilter: iconColor?.asFilter() + ); + } else { + childWidget = Text( + text, + textAlign: textAlign, + style: textStyle ?? const TextStyle( + fontSize: 12, + color: AppColor.colorTextButtonHeaderThread + ), + maxLines: maxLines, + overflow: maxLines == 1 ? CommonTextStyle.defaultTextOverFlow : null, + softWrap: maxLines == 1 ? CommonTextStyle.defaultSoftWrap : null, + ); + } + + return TMailContainerWidget( + onTapActionCallback: onTapActionCallback, + onTapActionAtPositionCallback: onTapActionAtPositionCallback, + borderRadius: borderRadius, + width: width, + maxWidth: maxWidth, + maxHeight: maxHeight, + minWidth: minWidth, + tooltipMessage: tooltipMessage, + backgroundColor: backgroundColor, + padding: padding, + margin: margin, + boxShadow: boxShadow, + border: border, + child: childWidget, + ); + } +} \ No newline at end of file diff --git a/core/lib/presentation/views/clipper/side_arrow_clipper.dart b/core/lib/presentation/views/clipper/side_arrow_clipper.dart new file mode 100644 index 0000000000..520f6f277b --- /dev/null +++ b/core/lib/presentation/views/clipper/side_arrow_clipper.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; + +/// Create a custom clipper with a side arrow. +/// To help achieve various custom shapes of widget +class SideArrowClipper extends CustomClipper { + /// Alignment + final bool isRight; + + ///The radius, which creates the curved appearance of the widget has a default value of 16. + final double radius; + + /// The arrow creates the curved shape of the widget and has a default arrowSize of 8. + final double arrowSize; + + /// Offset show distance from bottom and has default value 30. + final double offset; + + SideArrowClipper({ + this.isRight = false, + this.radius = 16, + this.offset = 30, + this.arrowSize = 8 + }); + + @override + Path getClip(Size size) { + var path = Path(); + + if (isRight) { + path.addRRect(RRect.fromLTRBR(0, 0, size.width - arrowSize, size.height, Radius.circular(radius))); + + var path2 = Path(); + path2.lineTo(arrowSize, arrowSize); + path2.lineTo(0, 2 * arrowSize); + path2.lineTo(0, 0); + + path.addPath(path2, Offset(size.width - arrowSize, size.height - offset - 2 * arrowSize)); + } else { + path.addRRect(RRect.fromLTRBR(arrowSize, 0, size.width, size.height, Radius.circular(radius))); + + var path2 = Path(); + path2.lineTo(0, 2 * arrowSize); + path2.lineTo(-arrowSize, arrowSize); + path2.lineTo(0, 0); + + path.addPath(path2, Offset(arrowSize, size.height - offset - 2 * arrowSize)); + } + + return path; + } + + @override + bool shouldReclip(CustomClipper oldClipper) => false; +} \ No newline at end of file diff --git a/core/lib/presentation/views/container/tmail_container_widget.dart b/core/lib/presentation/views/container/tmail_container_widget.dart new file mode 100644 index 0000000000..307bdc3df8 --- /dev/null +++ b/core/lib/presentation/views/container/tmail_container_widget.dart @@ -0,0 +1,107 @@ + +import 'package:core/presentation/action/action_callback_define.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class TMailContainerWidget extends StatelessWidget { + + final OnTapActionCallback? onTapActionCallback; + final OnTapActionAtPositionCallback? onTapActionAtPositionCallback; + + final Widget child; + final double borderRadius; + final double? width; + final double maxWidth; + final double maxHeight; + final double minWidth; + final String? tooltipMessage; + final Color? backgroundColor; + final EdgeInsetsGeometry? padding; + final EdgeInsetsGeometry? margin; + final List? boxShadow; + final BoxBorder? border; + + const TMailContainerWidget({ + super.key, + required this.child, + this.onTapActionCallback, + this.onTapActionAtPositionCallback, + this.borderRadius = 20, + this.width, + this.maxWidth = double.infinity, + this.maxHeight = double.infinity, + this.minWidth = 0, + this.tooltipMessage, + this.backgroundColor, + this.padding, + this.boxShadow, + this.margin, + this.border, + }); + + @override + Widget build(BuildContext context) { + final materialChild = Material( + color: Colors.transparent, + child: InkWell( + onTap: onTapActionCallback, + onTapDown: (detail) { + if (onTapActionAtPositionCallback != null) { + final screenSize = MediaQuery.of(context).size; + final offset = detail.globalPosition; + final position = RelativeRect.fromLTRB( + offset.dx, + offset.dy, + screenSize.width - offset.dx, + screenSize.height - offset.dy, + ); + onTapActionAtPositionCallback!.call(position); + } + }, + borderRadius: BorderRadius.all(Radius.circular(borderRadius)), + child: tooltipMessage != null + ? Tooltip( + message: tooltipMessage, + child: Container( + decoration: BoxDecoration( + color: backgroundColor ?? AppColor.colorButtonHeaderThread, + borderRadius: BorderRadius.all(Radius.circular(borderRadius)), + border: border, + boxShadow: boxShadow + ), + width: width, + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: maxHeight, + minWidth: minWidth + ), + padding: padding ?? const EdgeInsetsDirectional.all(8), + child: child + ) + ) + : Container( + decoration: BoxDecoration( + color: backgroundColor ?? AppColor.colorButtonHeaderThread, + borderRadius: BorderRadius.all(Radius.circular(borderRadius)), + border: border, + boxShadow: boxShadow + ), + width: width, + constraints: BoxConstraints( + maxWidth: maxWidth, + maxHeight: maxHeight, + minWidth: minWidth + ), + padding: padding ?? const EdgeInsetsDirectional.all(8), + child: child + ) + ), + ); + + if (margin != null) { + return Padding(padding: margin!, child: materialChild); + } else { + return materialChild; + } + } +} \ No newline at end of file diff --git a/core/lib/presentation/views/dialog/color_picker_dialog_builder.dart b/core/lib/presentation/views/dialog/color_picker_dialog_builder.dart index 2b2e17899c..4e56e8c41b 100644 --- a/core/lib/presentation/views/dialog/color_picker_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/color_picker_dialog_builder.dart @@ -8,24 +8,26 @@ import 'package:pointer_interceptor/pointer_interceptor.dart'; typedef SelectColorActionCallback = Function(Color? colorSelected); class ColorPickerDialogBuilder { - final SelectColorActionCallback? setColorActionCallback; final VoidCallback? cancelActionCallback; final VoidCallback? resetToDefaultActionCallback; final BuildContext _context; final Color defaultColor; - final Color _currentColor; + final ValueNotifier _currentColor; final String? title; final String? textActionSetColor; final String? textActionCancel; final String? textActionResetDefault; + final Function(Color)? onSelected; - Color? _colorSelected; + bool _shouldUpdate = false; + Color _colorCode = Colors.black; ColorPickerDialogBuilder( this._context, this._currentColor, { + this.onSelected, this.title, this.textActionSetColor, this.textActionCancel, @@ -35,22 +37,23 @@ class ColorPickerDialogBuilder { this.cancelActionCallback, this.resetToDefaultActionCallback } - ) : _colorSelected = _currentColor; + ); Future show() async { await showDialog(context: _context, builder: (BuildContext context) { return PointerInterceptor( child: AlertDialog( - title: Text(title ?? '', - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - color: Colors.black)), - titleTextStyle: const TextStyle( + title: Text( + title ?? '', + textAlign: TextAlign.center, + style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 20, - color: Colors.black), + color: Colors.black)), + titleTextStyle: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Colors.black), titlePadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), actionsPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), @@ -59,27 +62,55 @@ class ColorPickerDialogBuilder { shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), scrollable: true, elevation: 10, - content: ColorPicker( - color: _currentColor, - onColorChanged: (color) => _colorSelected = color, - width: 40, - height: 40, - spacing: 0, - runSpacing: 0, - borderRadius: 0, - wheelDiameter: 165, - enableOpacity: false, - showColorCode: true, - colorCodeHasColor: true, - pickersEnabled: const { - ColorPickerType.wheel: true, + content: ValueListenableBuilder( + valueListenable: _currentColor, + builder: (context, _, __) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 500, + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(4)), + color: Colors.white + ), + child: Center( + child: Wrap(children: AppColor.listColorsPicker + .map((color) => _itemColorWidget(context, color)) + .toList(), + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: ColorCodeField( + color: _currentColor.value, + colorCodeHasColor: true, + shouldUpdate: _shouldUpdate, + onColorChanged: (Color color) { + if (AppColor.listColorsPicker.any((element) => element.value == color.value)) { + _shouldUpdate = true; + _currentColor.value = color; + } else { + _shouldUpdate = false; + _currentColor.value = Colors.black; + _colorCode = color; + } + }, + onEditFocused: (bool editInFocus) { + _shouldUpdate = editInFocus ? true : false; + }, + copyPasteBehavior: const ColorPickerCopyPasteBehavior( + parseShortHexCode: true, + ), + toolIcons: const ColorPickerActionButtons( + dialogActionButtons: true, + ), + ), + ), + ], + ); }, - copyPasteBehavior: const ColorPickerCopyPasteBehavior( - parseShortHexCode: true, - ), - actionButtons: const ColorPickerActionButtons( - dialogActionButtons: true, - ), ), actions: [ buildButtonWrapText( @@ -108,13 +139,42 @@ class ColorPickerDialogBuilder { radius: 5, height: 30, textStyle: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500), - onTap: () => setColorActionCallback?.call(_colorSelected)) + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500), + onTap: () { + if (!_shouldUpdate) { + setColorActionCallback?.call(_colorCode); + } else { + setColorActionCallback?.call(_currentColor.value); + } + }) ], ), ); }); } + + Widget _itemColorWidget(BuildContext context, Color color) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + _shouldUpdate = true; + _currentColor.value = color; + }, + child: Container( + decoration: BoxDecoration( + color: color, + border: Border.all( + color: _currentColor.value == color ? Colors.white : Colors.transparent, + width: 8, + ), + ), + width: 40, + height: 40, + ), + ), + ); + } } \ No newline at end of file diff --git a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart index e35ff3698a..556a94d501 100644 --- a/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/confirmation_dialog_builder.dart @@ -23,6 +23,7 @@ class ConfirmDialogBuilder { TextStyle? _styleTitle; TextStyle? _styleContent; double? _radiusButton; + double? heightButton; EdgeInsets? _paddingTitle; EdgeInsets? _paddingContent; EdgeInsets? _paddingButton; @@ -34,6 +35,7 @@ class ConfirmDialogBuilder { Alignment? _alignment; Color? _backgroundColor; bool showAsBottomSheet; + List? listTextSpan; OnConfirmButtonAction? _onConfirmButtonAction; OnCancelButtonAction? _onCancelButtonAction; @@ -41,7 +43,11 @@ class ConfirmDialogBuilder { ConfirmDialogBuilder( this._imagePath, - {this.showAsBottomSheet = false} + { + this.showAsBottomSheet = false, + this.listTextSpan, + this.heightButton, + } ); void key(Key key) { @@ -116,7 +122,7 @@ class ConfirmDialogBuilder { _heightDialog = value; } - void aligment(Alignment? alignment) { + void alignment(Alignment? alignment) { _alignment = alignment; } @@ -166,7 +172,7 @@ class ConfirmDialogBuilder { height: _heightDialog, decoration: const BoxDecoration( color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(16))), + borderRadius: BorderRadius.all(Radius.circular(16))), margin: _margin, child: Wrap(children: [ if (_onCloseButtonAction != null) @@ -175,12 +181,12 @@ class ConfirmDialogBuilder { child: Padding( padding: const EdgeInsets.only(top: 8, right: 8), child: buildIconWeb( - icon: SvgPicture.asset(_imagePath.icCloseMailbox, fit: BoxFit.fill), + icon: SvgPicture.asset(_imagePath.icCircleClose, fit: BoxFit.fill), onTap: () => _onCloseButtonAction?.call()) )), if (_iconWidget != null) Container( - margin: _marginIcon ?? const EdgeInsets.only(top: 24), + margin: _marginIcon ?? EdgeInsets.zero, alignment: Alignment.center, child: _iconWidget, ), @@ -204,6 +210,19 @@ class ConfirmDialogBuilder { style: _styleContent ?? const TextStyle(fontSize: 17.0, color: AppColor.colorMessageDialog) ), ), + ) + else if (listTextSpan != null) + Padding( + padding: _paddingContent ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 24), + child: Center( + child: RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: _styleContent ?? const TextStyle(fontSize: 17.0, color: AppColor.colorMessageDialog), + children: listTextSpan + ), + ), + ), ), Padding( padding: _paddingButton ?? const EdgeInsets.only(bottom: 16, left: 16, right: 16), @@ -214,6 +233,7 @@ class ConfirmDialogBuilder { name: _cancelText, bgColor: _colorCancelButton, radius: _radiusButton, + height: heightButton, textStyle: _styleTextCancelButton, action: _onCancelButtonAction)), if (_confirmText.isNotEmpty && _cancelText.isNotEmpty) const SizedBox(width: 16), @@ -222,6 +242,7 @@ class ConfirmDialogBuilder { name: _confirmText, bgColor: _colorConfirmButton, radius: _radiusButton, + height: heightButton, textStyle: _styleTextConfirmButton, action: _onConfirmButtonAction)) ] @@ -231,11 +252,16 @@ class ConfirmDialogBuilder { } Widget _buildButton({ - String? name, TextStyle? textStyle, Color? bgColor, double? radius, Function? action + String? name, + TextStyle? textStyle, + Color? bgColor, + double? radius, + double? height, + Function? action }) { return SizedBox( width: double.infinity, - height: 48, + height: height ?? 48, child: ElevatedButton( onPressed: () => action?.call(), style: ButtonStyle( diff --git a/core/lib/presentation/views/dialog/edit_text_dialog_builder.dart b/core/lib/presentation/views/dialog/edit_text_dialog_builder.dart index a3c9860aab..00536b195a 100644 --- a/core/lib/presentation/views/dialog/edit_text_dialog_builder.dart +++ b/core/lib/presentation/views/dialog/edit_text_dialog_builder.dart @@ -102,10 +102,10 @@ class EditTextDialogBuilder { textAlign: TextAlign.center), Padding( padding: const EdgeInsets.only(top: 20), - child: TextFormField( + child: TextFormFieldBuilder( keyboardType: TextInputType.visiblePassword, - onChanged: (value) => _onTextChanged(value, setState), - autofocus: true, + onTextChange: (value) => _onTextChanged(value, setState), + autoFocus: true, controller: _textController, decoration: InputDecoration( errorText: _error, diff --git a/core/lib/presentation/views/floating_button/scrolling_floating_button_animated.dart b/core/lib/presentation/views/floating_button/scrolling_floating_button_animated.dart index a9c1aac25c..a343389d57 100644 --- a/core/lib/presentation/views/floating_button/scrolling_floating_button_animated.dart +++ b/core/lib/presentation/views/floating_button/scrolling_floating_button_animated.dart @@ -57,7 +57,7 @@ class ScrollingFloatingButtonAnimated extends StatefulWidget { : super(key: key); @override - _ScrollingFloatingButtonAnimatedState createState() => + State createState() => _ScrollingFloatingButtonAnimatedState(); } @@ -93,10 +93,10 @@ class _ScrollingFloatingButtonAnimatedState /// Function to add listener for scroll void _handleScroll() { - ScrollController _scrollController = widget.scrollController!; - _scrollController.addListener(() { - if (_scrollController.position.pixels > widget.limitIndicator! && - _scrollController.position.userScrollDirection == + ScrollController scrollController = widget.scrollController!; + scrollController.addListener(() { + if (scrollController.position.pixels > widget.limitIndicator! && + scrollController.position.userScrollDirection == ScrollDirection.reverse) { if (widget.animateIcon!) _animationController.forward(); if (mounted) { @@ -104,8 +104,8 @@ class _ScrollingFloatingButtonAnimatedState _onTop = false; }); } - } else if (_scrollController.position.pixels <= widget.limitIndicator! && - _scrollController.position.userScrollDirection == + } else if (scrollController.position.pixels <= widget.limitIndicator! && + scrollController.position.userScrollDirection == ScrollDirection.forward) { if (widget.animateIcon!) _animationController.reverse(); if (mounted) { @@ -145,14 +145,14 @@ class _ScrollingFloatingButtonAnimatedState Container( padding: EdgeInsets.only(left: 16, right: _onTop ? 10 : 16), child: AnimatedBuilder( - child: widget.icon!, animation: _animationController, - builder: (BuildContext context, Widget? _widget) { + builder: (BuildContext context, Widget? widget) { return Transform.rotate( angle: (_animationController.value * 3 * math.pi) / 180, - child: _widget!, + child: widget!, ); - }), + }, + child: widget.icon!), ), ...(_onTop ? [ diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart index 02c73fa123..d650623d01 100644 --- a/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart @@ -16,6 +16,7 @@ class HtmlContentViewerOnWeb extends StatefulWidget { final double widthContent; final double heightContent; final HtmlViewerControllerForWeb controller; + final TextDirection? direction; /// Handler for mailto: links final Function(Uri?)? mailtoDelegate; @@ -31,10 +32,11 @@ class HtmlContentViewerOnWeb extends StatefulWidget { required this.controller, this.allowResizeToDocumentSize = true, this.mailtoDelegate, + this.direction, }) : super(key: key); @override - _HtmlContentViewerOnWebState createState() => _HtmlContentViewerOnWebState(); + State createState() => _HtmlContentViewerOnWebState(); } class _HtmlContentViewerOnWebState extends State { @@ -51,6 +53,7 @@ class _HtmlContentViewerOnWebState extends State { bool _isLoading = true; double minHeight = 100; double minWidth = 300; + final jsonEncoder = const JsonEncoder(); @override void initState() { @@ -65,7 +68,9 @@ class _HtmlContentViewerOnWebState extends State { @override void didUpdateWidget(covariant HtmlContentViewerOnWeb oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.contentHtml != oldWidget.contentHtml) { + log('_HtmlContentViewerOnWebState::didUpdateWidget():Old-Direction: ${oldWidget.direction} | Current-Direction: ${widget.direction}'); + if (widget.contentHtml != oldWidget.contentHtml || + widget.direction != oldWidget.direction) { createdViewId = _getRandString(10); widget.controller.viewId = createdViewId; _setUpWeb(); @@ -118,18 +123,16 @@ class _HtmlContentViewerOnWebState extends State { function handleOnClickLink(e) { let link = e.target; let textContent = e.target.textContent; - console.log("handleOnClickLink: " + link); - console.log("handleOnClickLink: " + textContent); - if (link && isValidUrl(link)) { + if (link && isValidMailtoLink(link)) { window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: OpenLink", "url": "" + link}), "*"); e.preventDefault(); - } else if (textContent && isValidUrl(textContent)) { + } else if (textContent && isValidMailtoLink(textContent)) { window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: OpenLink", "url": "" + textContent}), "*"); e.preventDefault(); } } - function isValidUrl(string) { + function isValidMailtoLink(string) { let url; try { @@ -138,7 +141,7 @@ class _HtmlContentViewerOnWebState extends State { return false; } - return url.protocol === "http:" || url.protocol === "https:" || url.protocol === "mailto:"; + return url.protocol === "mailto:"; } '''; @@ -167,7 +170,8 @@ class _HtmlContentViewerOnWebState extends State { minHeight: minHeight, minWidth: minWidth, styleCSS: tooltipLinkCss, - javaScripts: webViewActionScripts + scriptsDisableZoom); + javaScripts: webViewActionScripts + scriptsDisableZoom, + direction: widget.direction); return htmlTemplate; } @@ -184,15 +188,8 @@ class _HtmlContentViewerOnWebState extends State { ..style.width = '100%' ..style.height = '100%' ..onLoad.listen((event) async { - final dataGetHeight = {'type': 'toIframe: getHeight', 'view' : createdViewId}; - final dataGetWidth = {'type': 'toIframe: getWidth', 'view' : createdViewId}; - - const jsonEncoder = JsonEncoder(); - final jsonGetHeight = jsonEncoder.convert(dataGetHeight); - final jsonGetWidth = jsonEncoder.convert(dataGetWidth); - - html.window.postMessage(jsonGetHeight, '*'); - html.window.postMessage(jsonGetWidth, '*'); + _sendMessageToWebViewForGetHeight(); + _sendMessageToWebViewForGetWidth(); html.window.onMessage.listen((event) { var data = json.decode(event.data); @@ -225,24 +222,12 @@ class _HtmlContentViewerOnWebState extends State { } } - if (data['type'] != null && data['type'].contains('toDart: onChangeContent') && data['view'] == createdViewId) { - if (Scrollable.of(context) != null) { - Scrollable.of(context)!.position.ensureVisible( - context.findRenderObject()!, - duration: const Duration(milliseconds: 100), - curve: Curves.easeIn); - } - } - if (data['type'] != null && data['type'].contains('toDart: OpenLink') && data['view'] == createdViewId) { final link = data['url']; if (link != null && mounted) { - log('_HtmlContentViewerOnWebState::_setUpWeb(): OpenLink: $link'); final urlString = link as String; if (urlString.startsWith('mailto:')) { widget.mailtoDelegate?.call(Uri.parse(urlString)); - } else { - html.window.open(urlString, '_blank'); } } } @@ -260,48 +245,66 @@ class _HtmlContentViewerOnWebState extends State { @override Widget build(BuildContext context) { - return Stack( - children: [ - SizedBox( - height: actualHeight, - width: actualWidth, - child: _buildWebView(), - ), - if (_isLoading) Align(alignment: Alignment.topCenter, child: _buildLoadingView()) - ], - ); + return LayoutBuilder(builder: (context, constraint) { + minHeight = math.max(constraint.maxHeight, minHeight); + return Stack( + children: [ + if (_htmlData?.isNotEmpty == false) + const SizedBox.shrink() + else + FutureBuilder( + future: webInit, + builder: (context, snapshot) { + if (snapshot.hasData) { + return SizedBox( + height: actualHeight, + width: actualWidth, + child: HtmlElementView( + key: ValueKey(_htmlData), + viewType: createdViewId, + ), + ); + } else { + return const SizedBox.shrink(); + } + } + ), + if (_isLoading) + const Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.all(16), + child: SizedBox( + width: 30, + height: 30, + child: CupertinoActivityIndicator( + color: AppColor.colorLoading + ) + ) + ) + ) + ], + ); + }); } - Widget _buildLoadingView() { - return const Padding( - padding: EdgeInsets.all(16), - child: SizedBox( - width: 30, - height: 30, - child: CupertinoActivityIndicator(color: AppColor.colorLoading))); + void _sendMessageToWebViewForGetHeight() { + final dataGetHeight = { + 'type': 'toIframe: getHeight', + 'view' : createdViewId + }; + final jsonGetHeight = jsonEncoder.convert(dataGetHeight); + + html.window.postMessage(jsonGetHeight, '*'); } - Widget _buildWebView() { - final htmlData = _htmlData; - if (htmlData == null || htmlData.isEmpty) { - return Container(); - } + void _sendMessageToWebViewForGetWidth() { + final dataGetWidth = { + 'type': 'toIframe: getWidth', + 'view' : createdViewId + }; + final jsonGetWidth = jsonEncoder.convert(dataGetWidth); - return Directionality( - textDirection: TextDirection.ltr, - child: FutureBuilder( - future: webInit, - builder: (context, snapshot) { - if (snapshot.hasData) { - return HtmlElementView( - key: ValueKey(htmlData), - viewType: createdViewId, - ); - } else { - return Container(); - } - } - ) - ); + html.window.postMessage(jsonGetWidth, '*'); } } \ No newline at end of file diff --git a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart index c420143d60..3a3b1dd75f 100644 --- a/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart +++ b/core/lib/presentation/views/html_viewer/html_content_viewer_widget.dart @@ -1,17 +1,18 @@ import 'dart:async'; import 'dart:io'; -import 'dart:ui'; +import 'dart:math'; -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/utils/html_transformer/html_event_action.dart'; +import 'package:core/presentation/utils/html_transformer/html_template.dart'; import 'package:core/presentation/utils/html_transformer/html_utils.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:url_launcher/url_launcher.dart' as launcher; import 'package:url_launcher/url_launcher_string.dart'; -import 'package:webview_flutter/webview_flutter.dart'; typedef OnScrollHorizontalEnd = Function(bool leftDirection); typedef OnWebViewLoaded = Function(bool isScrollPageViewActivated); @@ -22,9 +23,10 @@ class HtmlContentViewer extends StatefulWidget { final double heightContent; final OnScrollHorizontalEnd? onScrollHorizontalEnd; final OnWebViewLoaded? onWebViewLoaded; + final TextDirection? direction; - /// Register this callback if you want a reference to the [WebViewController]. - final void Function(WebViewController controller)? onCreated; + /// Register this callback if you want a reference to the [InAppWebViewController]. + final void Function(InAppWebViewController controller)? onCreated; /// Handler for mailto: links final Future Function(Uri mailto)? mailtoDelegate; @@ -43,6 +45,7 @@ class HtmlContentViewer extends StatefulWidget { this.onScrollHorizontalEnd, this.urlLauncherDelegate, this.mailtoDelegate, + this.direction, }) : super(key: key); @override @@ -52,11 +55,9 @@ class HtmlContentViewer extends StatefulWidget { class _HtmlContentViewState extends State { late double actualHeight; - double minHeight = 100; double minWidth = 300; - late double maxHeightForAndroid; String? _htmlData; - late WebViewController _webViewController; + late InAppWebViewController _webViewController; bool _isLoading = true; bool horizontalGestureActivated = false; @@ -64,15 +65,22 @@ class _HtmlContentViewState extends State { void initState() { super.initState(); actualHeight = widget.heightContent; - maxHeightForAndroid = window.physicalSize.height; - _htmlData = generateHtml(widget.contentHtml); + _htmlData = generateHtml( + widget.contentHtml, + direction: widget.direction, + ); } @override void didUpdateWidget(covariant HtmlContentViewer oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.contentHtml != oldWidget.contentHtml) { - _htmlData = generateHtml(widget.contentHtml); + log('_HtmlContentViewState::didUpdateWidget():Old-Direction: ${oldWidget.direction} | Current-Direction: ${widget.direction}'); + if (widget.contentHtml != oldWidget.contentHtml || + widget.direction != oldWidget.direction) { + _htmlData = generateHtml( + widget.contentHtml, + direction: widget.direction, + ); } } @@ -81,119 +89,118 @@ class _HtmlContentViewState extends State { return LayoutBuilder(builder: (context, constraints) { return Stack( children: [ - SizedBox( - height: actualHeight, - width: constraints.maxWidth, - child: _buildWebView()), + if (_htmlData?.isNotEmpty == false) + const SizedBox.shrink() + else + SizedBox( + height: actualHeight, + width: constraints.maxWidth, + child: InAppWebView( + key: ValueKey(_htmlData), + initialSettings: InAppWebViewSettings( + transparentBackground: true, + ), + onWebViewCreated: (controller) async { + _webViewController = controller; + await controller.loadData(data: _htmlData ?? ''); + widget.onCreated?.call(controller); + }, + onLoadStop: _onLoadStop, + onContentSizeChanged: _onContentSizeChanged, + shouldOverrideUrlLoading: _shouldOverrideUrlLoading, + gestureRecognizers: { + Factory(() => LongPressGestureRecognizer()), + if (Platform.isIOS && horizontalGestureActivated) + Factory(() => HorizontalDragGestureRecognizer()), + if (Platform.isAndroid) + Factory(() => ScaleGestureRecognizer()), + }, + onScrollChanged: (controller, x, y) => controller.scrollTo(x: 0, y: 0) + ), + ), if (_isLoading) - Align( - alignment: Alignment.center, - child: _buildLoadingView() + const Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.all(16), + child: SizedBox( + width: 30, + height: 30, + child: CupertinoActivityIndicator( + color: AppColor.colorLoading + ) + ) + ) ) ], ); }); } - Widget _buildLoadingView() { - return const Padding( - padding: EdgeInsets.all(16), - child: SizedBox( - width: 30, - height: 30, - child: CupertinoActivityIndicator(color: AppColor.colorLoading))); - } - - Widget _buildWebView() { - final htmlData = _htmlData; - if (htmlData == null || htmlData.isEmpty) { - return Container(); - } - return WebView( - key: ValueKey(htmlData), - javascriptMode: JavascriptMode.unrestricted, - backgroundColor: Colors.white, - onWebViewCreated: (controller) async { - _webViewController = controller; - await controller.loadHtmlString(htmlData, baseUrl: null); - widget.onCreated?.call(controller); - }, - onPageFinished: _onPageFinished, - zoomEnabled: false, - navigationDelegate: _onNavigation, - gestureRecognizers: { - Factory(() => LongPressGestureRecognizer()), - if (Platform.isIOS && horizontalGestureActivated) - Factory(() => HorizontalDragGestureRecognizer()), - if (Platform.isAndroid) - Factory(() => ScaleGestureRecognizer()), - }, - javascriptChannels: { - JavascriptChannel( - name: HtmlUtils.scrollEventJSChannelName, - onMessageReceived: _onHandleScrollEvent - ) - }, - ); - } - - void _onPageFinished(String url) async { + void _onLoadStop(InAppWebViewController controller, WebUri? webUri) async { await Future.wait([ _setActualHeightView(), _setActualWidthView(), ]); _hideLoadingProgress(); + + controller.addJavaScriptHandler( + handlerName: HtmlUtils.scrollEventJSChannelName, + callback: _onHandleScrollEvent + ); } - void _onHandleScrollEvent(JavascriptMessage javascriptMessage) { - log('_HtmlContentViewState::_onHandleScrollEvent():message: ${javascriptMessage.message}'); - if (javascriptMessage.message == HtmlEventAction.scrollRightEndAction) { - widget.onScrollHorizontalEnd?.call(false); - } else if (javascriptMessage.message == HtmlEventAction.scrollLeftEndAction) { + void _onContentSizeChanged( + InAppWebViewController controller, + Size oldContentSize, + Size newContentSize + ) async { + log('_HtmlContentViewState::_onContentSizeChanged:oldContentSize: $oldContentSize | newContentSize: $newContentSize'); + final maxContentHeight = max(oldContentSize.height, newContentSize.height); + log('_HtmlContentViewState::_onContentSizeChanged:maxContentHeight: $maxContentHeight'); + if (maxContentHeight > actualHeight) { + setState(() { + actualHeight = maxContentHeight; + }); + } + } + + void _onHandleScrollEvent(List parameters) { + log('_HtmlContentViewState::_onHandleScrollRightEvent():parameters: $parameters'); + final message = parameters.first; + log('_HtmlContentViewState::_onHandleScrollRightEvent():message: $message'); + if (message == HtmlEventAction.scrollLeftEndAction) { widget.onScrollHorizontalEnd?.call(true); + } else if (message == HtmlEventAction.scrollRightEndAction) { + widget.onScrollHorizontalEnd?.call(false); } } Future _setActualHeightView() async { - final scrollHeightText = await _webViewController.runJavascriptReturningResult('document.body.scrollHeight'); - final scrollHeight = double.tryParse(scrollHeightText); - log('_HtmlContentViewState::_setActualHeightView(): scrollHeightText: $scrollHeightText'); + final scrollHeight = await _webViewController.evaluateJavascript(source: 'document.body.scrollHeight'); if (scrollHeight != null && mounted) { final scrollHeightWithBuffer = scrollHeight + 30.0; - if (scrollHeightWithBuffer > minHeight) { + if (scrollHeightWithBuffer > actualHeight) { setState(() { - // It hotfix for web_view crash on android device and waiting lib web_view update to fix this issue - if (Platform.isAndroid && scrollHeightWithBuffer > maxHeightForAndroid){ - actualHeight = maxHeightForAndroid; - } else { - actualHeight = scrollHeightWithBuffer; - } + actualHeight = scrollHeightWithBuffer; _isLoading = false; }); - } else { - actualHeight = minHeight; } } - - return Future.value(null); } Future _setActualWidthView() async { final result = await Future.wait([ - _webViewController.runJavascriptReturningResult('document.getElementsByClassName("tmail-content")[0].scrollWidth'), - _webViewController.runJavascriptReturningResult('document.getElementsByClassName("tmail-content")[0].offsetWidth') + _webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0].scrollWidth'), + _webViewController.evaluateJavascript(source: 'document.getElementsByClassName("tmail-content")[0].offsetWidth') ]); if (result.length == 2) { - final scrollWidth = double.tryParse(result[0]); - final offsetWidth = double.tryParse(result[1]); - log('_HtmlContentViewState::_setActualWidthView():scrollWidth: $scrollWidth'); - log('_HtmlContentViewState::_setActualWidthView():offsetWidth: $offsetWidth'); - + final scrollWidth = result[0]; + final offsetWidth = result[1]; if (scrollWidth != null && offsetWidth != null && mounted) { final isScrollActivated = scrollWidth.round() == offsetWidth.round(); - log('_HtmlContentViewState::_setActualWidthView():isScrollActivated: $isScrollActivated'); if (isScrollActivated) { setState(() { horizontalGestureActivated = false; @@ -203,14 +210,12 @@ class _HtmlContentViewState extends State { horizontalGestureActivated = true; }); - await _webViewController.runJavascript(HtmlUtils.runScriptsHandleScrollEvent); + await _webViewController.evaluateJavascript(source: HtmlUtils.runScriptsHandleScrollEvent); } widget.onWebViewLoaded?.call(isScrollActivated); } } - - return Future.value(null); } void _hideLoadingProgress() { @@ -221,21 +226,31 @@ class _HtmlContentViewState extends State { } } - FutureOr _onNavigation(NavigationRequest navigation) async { - if (navigation.isForMainFrame && navigation.url == 'about:blank') { - return NavigationDecision.navigate; + Future _shouldOverrideUrlLoading( + InAppWebViewController controller, + NavigationAction navigationAction + ) async { + final url = navigationAction.request.url?.toString(); + + if (url == null) { + return NavigationActionPolicy.CANCEL; + } + + if (navigationAction.isForMainFrame && url == 'about:blank') { + return NavigationActionPolicy.ALLOW; } - final requestUri = Uri.parse(navigation.url); + + final requestUri = Uri.parse(url); final mailtoHandler = widget.mailtoDelegate; if (mailtoHandler != null && requestUri.isScheme('mailto')) { await mailtoHandler(requestUri); - return NavigationDecision.prevent; + return NavigationActionPolicy.CANCEL; } - final url = navigation.url; + final urlDelegate = widget.urlLauncherDelegate; if (urlDelegate != null) { await urlDelegate(Uri.parse(url)); - return NavigationDecision.prevent; + return NavigationActionPolicy.CANCEL; } if (await launcher.canLaunchUrl(Uri.parse(url))) { await launcher.launchUrl( @@ -243,6 +258,7 @@ class _HtmlContentViewState extends State { mode: LaunchMode.externalApplication ); } - return NavigationDecision.prevent; + + return NavigationActionPolicy.CANCEL; } } \ No newline at end of file diff --git a/core/lib/presentation/views/list/tree_view.dart b/core/lib/presentation/views/list/tree_view.dart index aeeead241f..b3c2db2569 100644 --- a/core/lib/presentation/views/list/tree_view.dart +++ b/core/lib/presentation/views/list/tree_view.dart @@ -57,6 +57,7 @@ class TreeViewChild { final Widget parent; final List children; final VoidCallback? onTap; + final EdgeInsetsGeometry? paddingChild; TreeViewChild( this.context, @@ -65,6 +66,7 @@ class TreeViewChild { required this.children, this.isExpanded, this.onTap, + this.paddingChild, Key? key, } ); @@ -86,7 +88,13 @@ class TreeViewChild { child: isExpanded! ? Column( mainAxisSize: MainAxisSize.min, - children: children.map((child) => Padding(padding: const EdgeInsets.only(left: 20), child: child)).toList()) + children: children + .map((child) => Padding( + padding: paddingChild ?? const EdgeInsetsDirectional.only(start: 20), + child: child + )) + .toList() + ) : const Offstage(), ), ], diff --git a/core/lib/presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart b/core/lib/presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart index b7a8c62be8..ee94e137a0 100644 --- a/core/lib/presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart +++ b/core/lib/presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/text/text_form_field_builder.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -126,10 +127,10 @@ class EditTextModalSheetBuilder { textAlign: TextAlign.center), Padding( padding: const EdgeInsets.only(top: 20), - child: TextFormField( + child: TextFormFieldBuilder( keyboardType: TextInputType.visiblePassword, - onChanged: (value) => _onTextChanged(value, setState), - autofocus: true, + onTextChange: (value) => _onTextChanged(value, setState), + autoFocus: true, controller: _textController, decoration: InputDecoration( errorText: _error, diff --git a/core/lib/presentation/views/popup_menu/popup_menu_item_widget.dart b/core/lib/presentation/views/popup_menu/popup_menu_item_widget.dart index b9c94aecbe..e59c0f90d5 100644 --- a/core/lib/presentation/views/popup_menu/popup_menu_item_widget.dart +++ b/core/lib/presentation/views/popup_menu/popup_menu_item_widget.dart @@ -1,4 +1,5 @@ +import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -28,7 +29,13 @@ class PopupMenuItemWidget extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: SizedBox( child: Row(children: [ - SvgPicture.asset(icon, width: 20, height: 20, fit: BoxFit.fill, color: iconColor), + SvgPicture.asset( + icon, + width: 20, + height: 20, + fit: BoxFit.fill, + colorFilter: iconColor.asFilter() + ), const SizedBox(width: 12), Expanded(child: Text(name, style: const TextStyle(fontSize: 15, color: Colors.black, fontWeight: FontWeight.w500))), diff --git a/core/lib/presentation/views/quick_search/quick_search_input_form.dart b/core/lib/presentation/views/quick_search/quick_search_input_form.dart index e32d420974..4d0a00f8b1 100644 --- a/core/lib/presentation/views/quick_search/quick_search_input_form.dart +++ b/core/lib/presentation/views/quick_search/quick_search_input_form.dart @@ -5,6 +5,7 @@ import 'dart:io'; import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -44,10 +45,6 @@ class QuickSearchInputForm extends FormField { {Key? key, String? initialValue, bool getImmediateSuggestions = false, - @Deprecated('Use autovalidateMode parameter which provides more specific ' - 'behavior related to auto validation. ' - 'This feature was deprecated after Flutter v1.19.0.') - bool autovalidate = false, bool enabled = true, AutovalidateMode autovalidateMode = AutovalidateMode.disabled, FormFieldSetter? onSaved, @@ -89,6 +86,7 @@ class QuickSearchInputForm extends FormField { bool hideSuggestionsBox = false, BoxDecoration? decoration, double? maxHeight, + bool isDirectionRTL = false, }) : assert( initialValue == null || textFieldConfiguration.controller == null), assert(minCharsForSuggestions >= 0), @@ -152,11 +150,12 @@ class QuickSearchInputForm extends FormField { hideSuggestionsBox: hideSuggestionsBox, decoration: decoration, maxHeight: maxHeight, + isDirectionRTL: isDirectionRTL, ); }); @override - _TypeAheadFormFieldState createState() => _TypeAheadFormFieldState(); + FormFieldState createState() => _TypeAheadFormFieldState(); } class _TypeAheadFormFieldState extends FormFieldState { @@ -519,6 +518,8 @@ class TypeAheadFieldQuickSearch extends StatefulWidget { /// Max height search input final double? maxHeight; + final bool isDirectionRTL; + /// Creates a [TypeAheadFieldQuickSearch] const TypeAheadFieldQuickSearch( {Key? key, @@ -560,6 +561,7 @@ class TypeAheadFieldQuickSearch extends StatefulWidget { this.hideSuggestionsBox = false, this.decoration, this.maxHeight, + this.isDirectionRTL = false, }) : assert(animationStart >= 0.0 && animationStart <= 1.0), assert( direction == AxisDirection.down || direction == AxisDirection.up), @@ -567,7 +569,7 @@ class TypeAheadFieldQuickSearch extends StatefulWidget { super(key: key); @override - _TypeAheadFieldQuickSearchState createState() => _TypeAheadFieldQuickSearchState(); + State> createState() => _TypeAheadFieldQuickSearchState(); } class _TypeAheadFieldQuickSearchState extends State> @@ -575,6 +577,7 @@ class _TypeAheadFieldQuickSearchState extends State widget.textFieldConfiguration.controller ?? _textEditingController; @@ -621,6 +624,8 @@ class _TypeAheadFieldQuickSearchState extends State extends State extends State extends State extends State extends State extends StatefulWidget { final RecentSelectionCallback? onRecentSelected; final EdgeInsets? listActionPadding; final bool hideSuggestionsBox; + final bool isDirectionRTL; const _SuggestionsList({ required this.suggestionsBox, @@ -932,6 +948,7 @@ class _SuggestionsList extends StatefulWidget { this.onRecentSelected, this.listActionPadding, this.hideSuggestionsBox = false, + this.isDirectionRTL = false, }); @override @@ -1174,9 +1191,12 @@ class _SuggestionsListState extends State<_SuggestionsList> children: widget.listActionButton!.map((dynamic action) { if (widget.actionButtonBuilder != null) { return Padding( - padding: const EdgeInsets.only(right: 8, bottom: kIsWeb ? 8 : 0), + padding: EdgeInsets.only( + right: widget.isDirectionRTL ? 0 : 8, + left: widget.isDirectionRTL ? 8 : 0, + bottom: kIsWeb ? 8 : 0 + ), child: InkWell( - child: widget.actionButtonBuilder!(context, action), borderRadius: const BorderRadius.all(Radius.circular(10)), onTap: () { if (widget.buttonActionCallback != null) { @@ -1184,6 +1204,7 @@ class _SuggestionsListState extends State<_SuggestionsList> invalidateSuggestions(); } }, + child: widget.actionButtonBuilder!(context, action), ), ); } else { @@ -1255,9 +1276,12 @@ class _SuggestionsListState extends State<_SuggestionsList> children: widget.listActionButton!.map((dynamic action) { if (widget.actionButtonBuilder != null) { return Padding( - padding: const EdgeInsets.only(right: 8, bottom: kIsWeb ? 8 : 0), + padding: EdgeInsets.only( + right: widget.isDirectionRTL ? 0 : 8, + left: widget.isDirectionRTL ? 8 : 0, + bottom: kIsWeb ? 8 : 0 + ), child: InkWell( - child: widget.actionButtonBuilder!(context, action), borderRadius: const BorderRadius.all(Radius.circular(10)), onTap: () { if (widget.buttonActionCallback != null) { @@ -1265,6 +1289,7 @@ class _SuggestionsListState extends State<_SuggestionsList> invalidateSuggestions(); } }, + child: widget.actionButtonBuilder!(context, action), ), ); } else { @@ -1680,7 +1705,7 @@ class _SuggestionsBox { if (isOpened) return; assert(_overlayEntry != null); resize(); - Overlay.of(context)!.insert(_overlayEntry!); + Overlay.of(context).insert(_overlayEntry!); isOpened = true; } @@ -1726,7 +1751,7 @@ class _SuggestionsBox { await Future.delayed(const Duration(milliseconds: 170)); timer += 170; - if (widgetMounted && + if (widgetMounted && context.mounted && (MediaQuery.of(context).viewInsets != initial || _findRootMediaQuery() != initialRootMediaQuery)) { return true; diff --git a/core/lib/presentation/views/responsive/responsive_widget.dart b/core/lib/presentation/views/responsive/responsive_widget.dart index fbf0e737aa..cdd25b2ed5 100644 --- a/core/lib/presentation/views/responsive/responsive_widget.dart +++ b/core/lib/presentation/views/responsive/responsive_widget.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; class ResponsiveWidget extends StatelessWidget { @@ -24,7 +25,7 @@ class ResponsiveWidget extends StatelessWidget { @override Widget build(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (responsiveUtils.isMobile(context)) { return mobile; } diff --git a/core/lib/presentation/views/tab_bar/custom_tab_indicator.dart b/core/lib/presentation/views/tab_bar/custom_tab_indicator.dart deleted file mode 100644 index 54ebb74798..0000000000 --- a/core/lib/presentation/views/tab_bar/custom_tab_indicator.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/widgets.dart'; - -enum CustomIndicatorSize { - tiny, - normal, - full, -} - -class CustomIndicator extends Decoration { - final double indicatorHeight; - final Color indicatorColor; - final CustomIndicatorSize indicatorSize; - - const CustomIndicator({ - required this.indicatorHeight, - required this.indicatorColor, - required this.indicatorSize - }); - - @override - _CustomPainter createBoxPainter([VoidCallback? onChanged]) { - return _CustomPainter(this, onChanged); - } -} - -class _CustomPainter extends BoxPainter { - final CustomIndicator decoration; - - _CustomPainter(this.decoration, VoidCallback? onChanged) : super(onChanged); - - @override - void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { - assert(configuration.size != null); - - Rect? rect; - if (decoration.indicatorSize == CustomIndicatorSize.full) { - rect = Offset(offset.dx, - (configuration.size!.height - decoration.indicatorHeight)) & - Size(configuration.size!.width, decoration.indicatorHeight); - } else if (decoration.indicatorSize == CustomIndicatorSize.normal) { - rect = Offset(offset.dx + 6, - (configuration.size!.height - decoration.indicatorHeight)) & - Size(configuration.size!.width - 12, decoration.indicatorHeight); - } else if (decoration.indicatorSize == CustomIndicatorSize.tiny) { - rect = Offset(offset.dx + configuration.size!.width / 2 - 8, - (configuration.size!.height - decoration.indicatorHeight)) & - Size(16, decoration.indicatorHeight); - } - - if (rect != null) { - final Paint paint = Paint(); - paint.color = decoration.indicatorColor; - paint.style = PaintingStyle.fill; - canvas.drawRRect( - RRect.fromRectAndCorners(rect, - topRight: const Radius.circular(8), - topLeft: const Radius.circular(8)), - paint); - } - } -} diff --git a/core/lib/presentation/views/text/input_decoration_builder.dart b/core/lib/presentation/views/text/input_decoration_builder.dart index 9225bbbd93..5b5a7dcfc4 100644 --- a/core/lib/presentation/views/text/input_decoration_builder.dart +++ b/core/lib/presentation/views/text/input_decoration_builder.dart @@ -7,7 +7,7 @@ abstract class InputDecorationBuilder { TextStyle? labelStyle; String? hintText; TextStyle? hintStyle; - EdgeInsets? contentPadding; + EdgeInsetsGeometry? contentPadding; OutlineInputBorder? enabledBorder; OutlineInputBorder? errorBorder; OutlineInputBorder? focusBorder; @@ -43,7 +43,7 @@ abstract class InputDecorationBuilder { hintStyle = newHintStyle; } - void setContentPadding(EdgeInsets? newContentPadding) { + void setContentPadding(EdgeInsetsGeometry? newContentPadding) { contentPadding = newContentPadding; } diff --git a/core/lib/presentation/views/text/slogan_builder.dart b/core/lib/presentation/views/text/slogan_builder.dart index c2ca250979..de12420232 100644 --- a/core/lib/presentation/views/text/slogan_builder.dart +++ b/core/lib/presentation/views/text/slogan_builder.dart @@ -1,3 +1,4 @@ +import 'package:core/presentation/utils/style_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -7,94 +8,104 @@ import 'package:flutter_svg/flutter_svg.dart'; typedef OnTapCallback = void Function(); -class SloganBuilder { +class SloganBuilder extends StatelessWidget { final bool arrangedByHorizontal; - - Key? _key; - String? _text; - TextStyle? _textStyle; - TextAlign? _textAlign; - String? _logoSVG; - String? _logo; - double? _sizeLogo; - OnTapCallback? _onTapCallback; - EdgeInsetsGeometry? _padding; - - SloganBuilder({this.arrangedByHorizontal = false}); - - void key(Key key) { - _key = key; - } - - void setSloganText(String text) { - _text = text; - } - - void setSloganTextStyle(TextStyle textStyle) { - _textStyle = textStyle; - } - - void setSloganTextAlign(TextAlign textAlign) { - _textAlign = textAlign; - } - - void setLogo(String logo) { - _logo = logo; - } - - void setLogoSVG(String logoSVG) { - _logoSVG = logoSVG; - } - - void setSizeLogo(double? size) { - _sizeLogo = size; - } - - void addOnTapCallback(OnTapCallback? onTapCallback) { - _onTapCallback = onTapCallback; - } - - void setPadding(EdgeInsetsGeometry? padding) { - _padding = padding; - } - - Widget build() { + final String? text; + final TextStyle? textStyle; + final TextAlign? textAlign; + final String? logoSVG; + final String? logo; + final double? sizeLogo; + final OnTapCallback? onTapCallback; + final EdgeInsetsGeometry? paddingText; + final EdgeInsetsGeometry? padding; + final bool enableOverflow; + final Color? hoverColor; + final double? hoverRadius; + + const SloganBuilder({ + super.key, + this.arrangedByHorizontal = true, + this.enableOverflow = false, + this.text, + this.textStyle, + this.textAlign, + this.logoSVG, + this.logo, + this.sizeLogo, + this.onTapCallback, + this.padding, + this.paddingText, + this.hoverColor, + this.hoverRadius + }); + + @override + Widget build(BuildContext context) { if (!arrangedByHorizontal) { return InkWell( - onTap: () => _onTapCallback?.call(), - child: Column(children: [ - _logoApp(), - Padding( - padding: _padding ?? const EdgeInsets.only(top: 16, left: 16, right: 16), - child: Text(_text ?? '', key: _key, style: _textStyle, textAlign: _textAlign), - ), - ]), + onTap: onTapCallback, + hoverColor: hoverColor, + borderRadius: BorderRadius.all(Radius.circular(hoverRadius ?? 8)), + child: Padding( + padding: padding ?? EdgeInsets.zero, + child: Column(children: [ + _logoApp(), + Padding( + padding: paddingText ?? const EdgeInsetsDirectional.only(top: 16, start: 16, end: 16), + child: Text( + text ?? '', + style: textStyle, + textAlign: textAlign, + overflow: enableOverflow ? CommonTextStyle.defaultTextOverFlow : null, + softWrap: enableOverflow ? CommonTextStyle.defaultSoftWrap : null, + maxLines: enableOverflow ? 1 : null, + ), + ), + ]), + ), ); } else { return InkWell( - onTap: () => _onTapCallback?.call(), - child: Row(children: [ - _logoApp(), - Padding( - padding: _padding ?? const EdgeInsets.symmetric(horizontal: 10), - child: Text(_text ?? '', key: _key, style: _textStyle, textAlign: _textAlign), - ), - ]), + onTap: onTapCallback, + hoverColor: hoverColor, + radius: hoverRadius ?? 8, + borderRadius: BorderRadius.all(Radius.circular(hoverRadius ?? 8)), + child: Padding( + padding: padding ?? EdgeInsets.zero, + child: Row(children: [ + _logoApp(), + Padding( + padding: paddingText ?? const EdgeInsets.symmetric(horizontal: 10), + child: Text( + text ?? '', + style: textStyle, + textAlign: textAlign, + overflow: enableOverflow ? CommonTextStyle.defaultTextOverFlow : null, + softWrap: enableOverflow ? CommonTextStyle.defaultSoftWrap : null, + maxLines: enableOverflow ? 1 : null, + ), + ), + ]), + ), ); } } Widget _logoApp() { - if (_logoSVG != null) { - return SvgPicture.asset(_logoSVG!, width: _sizeLogo ?? 150, height: _sizeLogo ?? 150); - } else if (_logo != null) { + if (logoSVG != null) { + return SvgPicture.asset( + logoSVG!, + width: sizeLogo ?? 150, + height: sizeLogo ?? 150); + } else if (logo != null) { return Image( - image: AssetImage(_logo!), - fit: BoxFit.fill, - width: _sizeLogo ?? 150, - height: _sizeLogo ?? 150, - alignment: Alignment.center); + image: AssetImage(logo!), + fit: BoxFit.fill, + width: sizeLogo ?? 150, + height: sizeLogo ?? 150, + alignment: Alignment.center); } return const SizedBox.shrink(); } diff --git a/core/lib/presentation/views/text/text_builder.dart b/core/lib/presentation/views/text/text_builder.dart deleted file mode 100644 index 88e4b6fafb..0000000000 --- a/core/lib/presentation/views/text/text_builder.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:core/presentation/utils/style_utils.dart'; -import 'package:flutter/material.dart'; - -class TextBuilder { - String? _text; - TextStyle? _textStyle; - TextAlign? _textAlign; - Key? _key; - - TextBuilder key(Key key) { - _key = key; - return this; - } - - TextBuilder text(String text) { - _text = text; - return this; - } - - TextBuilder textStyle(TextStyle textStyle) { - _textStyle = textStyle; - return this; - } - - TextBuilder textAlign(TextAlign textAlign) { - _textAlign = textAlign; - return this; - } - - Text build() { - return Text(_text ?? '', key: _key ?? const Key('TextBuilder'), style: _textStyle ?? CommonTextStyle.textStyleNormal, textAlign: _textAlign ?? TextAlign.center); - } -} - -class CenterTextBuilder extends TextBuilder { - @override - Text build() { - return Text(_text ?? '', key: _key ?? const Key('TextBuilder'), style: _textStyle, textAlign: TextAlign.center); - } -} \ No newline at end of file diff --git a/core/lib/presentation/views/text/text_field_builder.dart b/core/lib/presentation/views/text/text_field_builder.dart index 82a9c98fde..dbbc70eed1 100644 --- a/core/lib/presentation/views/text/text_field_builder.dart +++ b/core/lib/presentation/views/text/text_field_builder.dart @@ -1,100 +1,115 @@ import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; -class TextFieldBuilder { - Key? _key; - ValueChanged? _onTextChange; - ValueChanged? _onTextSubmitted; - TextStyle? _textStyle; - TextInputAction? _textInputAction; - InputDecoration? _inputDecoration; - bool? _obscureText; - int? _maxLines = 1; - int? _minLines; - TextEditingController? _textController; - TextInputType? _keyboardType; - Color? _cursorColor; - bool? _autoFocus; - FocusNode? _focusNode; - - void key(Key key) { - _key = key; - } - - void onChange(ValueChanged onChange) { - _onTextChange = onChange; - } - - void onSubmitted(ValueChanged onSubmitted) { - _onTextSubmitted = onSubmitted; - } - - void textStyle(TextStyle style) { - _textStyle = style; - } - - void textInputAction(TextInputAction inputAction) { - _textInputAction = inputAction; - } - - void textDecoration(InputDecoration inputDecoration) { - _inputDecoration = inputDecoration; - } - - void obscureText(bool obscureText) { - _obscureText = obscureText; - } - - void setText(String value) { - _textController = TextEditingController.fromValue(TextEditingValue(text: value)); - } - - void addController(TextEditingController? textEditingController) { - _textController = textEditingController; - } - - void maxLines(int? value) { - _maxLines = value; - } - - void minLines(int? value) { - _minLines = value; - } - - void keyboardType(TextInputType? value) { - _keyboardType = value; - } - - void cursorColor(Color? color) { - _cursorColor = color; - } - - void autoFocus(bool autoFocus) { - _autoFocus = autoFocus; - } - - void addFocusNode(FocusNode? focusNode) { - _focusNode = focusNode; - } - - TextField build() { +class TextFieldBuilder extends StatefulWidget { + + final ValueChanged? onTextChange; + final ValueChanged? onTextSubmitted; + final VoidCallback? onTap; + final TextStyle? textStyle; + final TextInputAction? textInputAction; + final InputDecoration? decoration; + final bool obscureText; + final int? maxLines; + final int? minLines; + final TextEditingController? controller; + final TextInputType? keyboardType; + final Color cursorColor; + final bool autoFocus; + final FocusNode? focusNode; + final String? fromValue; + final Brightness? keyboardAppearance; + final bool autocorrect; + final TextDirection textDirection; + final bool readOnly; + final MouseCursor? mouseCursor; + + const TextFieldBuilder({ + super.key, + this.cursorColor = AppColor.primaryColor, + this.autocorrect = false, + this.obscureText = false, + this.autoFocus = false, + this.readOnly = false, + this.textStyle = const TextStyle(color: AppColor.textFieldTextColor), + this.textDirection = TextDirection.ltr, + this.textInputAction, + this.decoration, + this.maxLines, + this.minLines, + this.controller, + this.keyboardType, + this.focusNode, + this.fromValue, + this.keyboardAppearance, + this.mouseCursor, + this.onTap, + this.onTextChange, + this.onTextSubmitted, + }); + + @override + State createState() => _TextFieldBuilderState(); +} + +class _TextFieldBuilderState extends State { + + late TextEditingController _controller; + late TextDirection _textDirection; + + @override + void initState() { + super.initState(); + if (widget.fromValue != null) { + _controller = TextEditingController.fromValue(TextEditingValue(text: widget.fromValue!)); + } else { + _controller = widget.controller ?? TextEditingController(); + } + _textDirection = widget.textDirection; + } + + @override + Widget build(BuildContext context) { return TextField( - key: _key ?? const Key('TextFieldBuilder'), - onChanged: _onTextChange, - cursorColor: _cursorColor ?? AppColor.primaryColor, - controller: _textController, - autocorrect: false, - textInputAction: _textInputAction, - decoration: _inputDecoration, - maxLines: _maxLines, - minLines: _minLines, - keyboardAppearance: Brightness.light, - style: _textStyle ?? const TextStyle(color: AppColor.textFieldTextColor), - obscureText: _obscureText ?? false, - keyboardType: _keyboardType, - onSubmitted: _onTextSubmitted, - autofocus: _autoFocus ?? false, - focusNode: _focusNode, + key: widget.key, + controller: _controller, + cursorColor: widget.cursorColor, + autocorrect: widget.autocorrect, + textInputAction: widget.textInputAction, + decoration: widget.decoration, + maxLines: widget.maxLines, + minLines: widget.minLines, + keyboardAppearance: widget.keyboardAppearance, + style: widget.textStyle, + obscureText: widget.obscureText, + keyboardType: widget.keyboardType, + autofocus: widget.autoFocus, + focusNode: widget.focusNode, + textDirection: _textDirection, + readOnly: widget.readOnly, + mouseCursor: widget.mouseCursor, + onChanged: (value) { + widget.onTextChange?.call(value); + if (value.isNotEmpty) { + final directionByText = DirectionUtils.getDirectionByEndsText(value); + if (directionByText != _textDirection) { + setState(() { + _textDirection = directionByText; + }); + } + } + }, + onSubmitted: widget.onTextSubmitted, + onTap: widget.onTap, ); } + + @override + void dispose() { + if (widget.fromValue == null && widget.controller == null) { + _controller.dispose(); + } + super.dispose(); + } } \ No newline at end of file diff --git a/core/lib/presentation/views/text/text_form_field_builder.dart b/core/lib/presentation/views/text/text_form_field_builder.dart new file mode 100644 index 0000000000..cc21b8070a --- /dev/null +++ b/core/lib/presentation/views/text/text_form_field_builder.dart @@ -0,0 +1,120 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:flutter/material.dart'; + +class TextFormFieldBuilder extends StatefulWidget { + + final ValueChanged? onTextChange; + final ValueChanged? onTextSubmitted; + final VoidCallback? onTap; + final TextStyle? textStyle; + final TextInputAction? textInputAction; + final InputDecoration? decoration; + final bool obscureText; + final int? maxLines; + final int? minLines; + final TextEditingController? controller; + final TextInputType? keyboardType; + final Color cursorColor; + final bool autoFocus; + final FocusNode? focusNode; + final String? fromValue; + final Brightness? keyboardAppearance; + final bool autocorrect; + final TextDirection textDirection; + final bool readOnly; + final MouseCursor? mouseCursor; + final List? autofillHints; + + const TextFormFieldBuilder({ + super.key, + this.cursorColor = AppColor.primaryColor, + this.autocorrect = false, + this.obscureText = false, + this.autoFocus = false, + this.readOnly = false, + this.textStyle = const TextStyle(color: AppColor.textFieldTextColor), + this.textDirection = TextDirection.ltr, + this.textInputAction, + this.decoration, + this.maxLines = 1, + this.minLines, + this.controller, + this.keyboardType, + this.focusNode, + this.fromValue, + this.keyboardAppearance, + this.mouseCursor, + this.autofillHints, + this.onTap, + this.onTextChange, + this.onTextSubmitted, + }); + + @override + State createState() => _TextFieldFormBuilderState(); +} + +class _TextFieldFormBuilderState extends State { + + late TextEditingController _controller; + late TextDirection _textDirection; + + @override + void initState() { + if (widget.fromValue != null) { + _controller = TextEditingController.fromValue(TextEditingValue(text: widget.fromValue!)); + } else if (widget.controller != null) { + _controller = widget.controller!; + } else { + _controller = TextEditingController(); + } + _textDirection = widget.textDirection; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return TextFormField( + key: widget.key, + controller: _controller, + cursorColor: widget.cursorColor, + autocorrect: widget.autocorrect, + textInputAction: widget.textInputAction, + decoration: widget.decoration, + maxLines: widget.maxLines, + minLines: widget.minLines, + keyboardAppearance: widget.keyboardAppearance, + style: widget.textStyle, + obscureText: widget.obscureText, + keyboardType: widget.keyboardType, + autofocus: widget.autoFocus, + focusNode: widget.focusNode, + textDirection: _textDirection, + readOnly: widget.readOnly, + mouseCursor: widget.mouseCursor, + autofillHints: widget.autofillHints, + onChanged: (value) { + widget.onTextChange?.call(value); + if (value.isNotEmpty) { + final directionByText = DirectionUtils.getDirectionByEndsText(value); + if (directionByText != _textDirection) { + setState(() { + _textDirection = directionByText; + }); + } + } + }, + onFieldSubmitted: widget.onTextSubmitted, + onTap: widget.onTap, + ); + } + + @override + void dispose() { + if (widget.fromValue == null && widget.controller == null) { + _controller.dispose(); + } + super.dispose(); + } +} \ No newline at end of file diff --git a/core/lib/presentation/views/text/text_overflow_builder.dart b/core/lib/presentation/views/text/text_overflow_builder.dart new file mode 100644 index 0000000000..d10030f0dc --- /dev/null +++ b/core/lib/presentation/views/text/text_overflow_builder.dart @@ -0,0 +1,41 @@ + +import 'package:core/presentation/extensions/string_extension.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:flutter/material.dart'; + +class TextOverflowBuilder extends StatelessWidget { + + final String data; + final TextStyle? style; + final TextAlign? textAlign; + final bool? softWrap; + final int? maxLines; + final TextDirection? textDirection; + final TextOverflow? overflow; + + const TextOverflowBuilder(this.data, { + super.key, + this.style, + this.textAlign, + this.softWrap = CommonTextStyle.defaultSoftWrap, + this.maxLines = 1, + this.textDirection, + this.overflow = CommonTextStyle.defaultTextOverFlow, + }); + + @override + Widget build(BuildContext context) { + return Text( + DirectionUtils.isDirectionRTLByLanguage(context) + ? data + : data.overflow, + style: style, + textAlign: textAlign, + softWrap: softWrap, + maxLines: maxLines, + textDirection: textDirection, + overflow: overflow, + ); + } +} \ No newline at end of file diff --git a/core/lib/presentation/views/text/type_ahead_form_field_builder.dart b/core/lib/presentation/views/text/type_ahead_form_field_builder.dart new file mode 100644 index 0000000000..01e64d93c9 --- /dev/null +++ b/core/lib/presentation/views/text/type_ahead_form_field_builder.dart @@ -0,0 +1,111 @@ +import 'package:core/utils/direction_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; + +class TypeAheadFormFieldBuilder extends StatefulWidget { + + final TextDirection textDirection; + final Duration debounceDuration; + final SuggestionsCallback suggestionsCallback; + final ItemBuilder itemBuilder; + final SuggestionSelectionCallback onSuggestionSelected; + final SuggestionsBoxDecoration suggestionsBoxDecoration; + final WidgetBuilder? noItemsFoundBuilder; + final bool hideOnEmpty; + final bool hideOnError; + final bool hideOnLoading; + final TextEditingController? controller; + final FocusNode? focusNode; + final ValueChanged? onTextChange; + final ValueChanged? onTextSubmitted; + final TextInputAction? textInputAction; + final bool autocorrect; + final List? autofillHints; + final TextInputType keyboardType; + final InputDecoration decoration; + + const TypeAheadFormFieldBuilder({ + super.key, + required this.suggestionsCallback, + required this.itemBuilder, + required this.onSuggestionSelected, + this.suggestionsBoxDecoration = const SuggestionsBoxDecoration(), + this.textDirection = TextDirection.ltr, + this.debounceDuration = const Duration(milliseconds: 300), + this.decoration = const InputDecoration(), + this.noItemsFoundBuilder, + this.hideOnEmpty = false, + this.hideOnError = false, + this.hideOnLoading = false, + this.autocorrect = false, + this.keyboardType = TextInputType.text, + this.controller, + this.focusNode, + this.autofillHints, + this.textInputAction, + this.onTextChange, + this.onTextSubmitted, + }); + + @override + State> createState() => _TypeAheadFormFieldBuilderState(); +} + +class _TypeAheadFormFieldBuilderState extends State> { + + late TextEditingController _controller; + late TextDirection _textDirection; + + @override + void initState() { + super.initState(); + _textDirection = widget.textDirection; + _controller = widget.controller ?? TextEditingController(); + } + + @override + Widget build(BuildContext context) { + return TypeAheadFormField( + key: widget.key, + textFieldConfiguration: TextFieldConfiguration( + controller: widget.controller, + textInputAction: widget.textInputAction, + autocorrect: widget.autocorrect, + autofillHints: widget.autofillHints, + keyboardType: widget.keyboardType, + decoration: widget.decoration, + focusNode: widget.focusNode, + textDirection: _textDirection, + onChanged: (value) { + widget.onTextChange?.call(value); + if (value.isNotEmpty) { + final directionByText = DirectionUtils.getDirectionByEndsText(value); + if (directionByText != _textDirection) { + setState(() { + _textDirection = directionByText; + }); + } + } + }, + onSubmitted: widget.onTextSubmitted + ), + debounceDuration: widget.debounceDuration, + suggestionsCallback: widget.suggestionsCallback, + itemBuilder: widget.itemBuilder, + onSuggestionSelected: widget.onSuggestionSelected, + suggestionsBoxDecoration: widget.suggestionsBoxDecoration, + noItemsFoundBuilder: widget.noItemsFoundBuilder, + hideOnEmpty: widget.hideOnEmpty, + hideOnError: widget.hideOnError, + hideOnLoading: widget.hideOnLoading, + ); + } + + @override + void dispose() { + if (widget.controller == null) { + _controller.dispose(); + } + super.dispose(); + } +} \ No newline at end of file diff --git a/core/lib/presentation/views/toast/tmail_toast.dart b/core/lib/presentation/views/toast/tmail_toast.dart index 0a30b44cfd..2ca0a1a8f6 100644 --- a/core/lib/presentation/views/toast/tmail_toast.dart +++ b/core/lib/presentation/views/toast/tmail_toast.dart @@ -1,147 +1,184 @@ import 'dart:async'; +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/views/toast/toast_position.dart'; import 'package:flutter/material.dart'; class TMailToast { - /// text of type [String] display on toast - String? text; - - /// defines the duration of time toast display over screen - int? toastDuration; - - /// defines the position of toast over the screen - ToastPosition? toastPosition; - - /// defines the background color of the toast - Color? backgroundColor; - - /// defines the test style of the toast text - TextStyle? textStyle; - - /// defines the border radius of the toast - double? toastBorderRadius; - - /// defines the border of the toast - Border? border; - - /// defines the trailing widget of the toast - Widget? trailing; - - /// defines the leading widget of the toast - Widget? leading; - - /// defines the size width widget of the toast - double? width; - - // ignore: type_annotate_public_apis, always_declare_return_types static showToast( - text, - BuildContext context, { - toastDuration, - toastPosition, - backgroundColor = const Color(0xAA000000), - textStyle = const TextStyle(fontSize: 15, color: Colors.white), - toastBorderRadius = 20.0, - border, - trailing, - leading, - width, - }) { - assert(text != null); + String text, + BuildContext context, { + Duration? toastDuration, + ToastPosition? toastPosition, + Color? backgroundColor, + TextStyle textStyle = const TextStyle( + fontSize: 15, + color: Colors.white, + fontWeight: FontWeight.normal + ), + double toastBorderRadius = 10.0, + Border? border, + Widget? trailing, + Widget? leading, + double maxWidth = 424, + EdgeInsets? padding, + TextAlign? textAlign + }) { ToastView.dismiss(); - ToastView.createView(text, context, toastPosition, - backgroundColor, textStyle, toastBorderRadius, border, trailing, - leading, width, toastDuration: toastDuration,); + ToastView.createView( + text, + context, + toastPosition, + backgroundColor, + textStyle, + toastBorderRadius, + border, + trailing, + leading, + maxWidth, + padding, + textAlign, + toastDuration: toastDuration, + ); } } class ToastView { static final ToastView _instance = ToastView._internal(); - // ignore: sort_constructors_first factory ToastView() => _instance; - // ignore: sort_constructors_first ToastView._internal(); static OverlayState? overlayState; static OverlayEntry? _overlayEntry; static bool _isVisible = false; - // ignore: avoid_void_async static void createView( String text, BuildContext context, ToastPosition? toastPosition, - Color backgroundColor, - TextStyle textStyle, + Color? backgroundColor, + TextStyle? textStyle, double toastBorderRadius, Border? border, Widget? trailing, Widget? leading, - double? width, - {int? toastDuration = 2} + double maxWidth, + EdgeInsets? padding, + TextAlign? textAlign, + {Duration? toastDuration} ) async { overlayState = Overlay.of(context, rootOverlay: false); - final Widget child = Center( child: Material( color: Colors.transparent, child: Container( - width: width, + constraints: BoxConstraints(maxWidth: maxWidth), decoration: BoxDecoration( - color: backgroundColor, + color: backgroundColor ?? Colors.white, borderRadius: BorderRadius.circular(toastBorderRadius), border: border, + boxShadow: const [ + BoxShadow( + color: AppColor.colorShadowLayerBottom, + blurRadius: 24, + offset: Offset.zero), + BoxShadow( + color: AppColor.colorShadowBgContentEmail, + blurRadius: 2, + offset: Offset.zero), + ] ), - margin: const EdgeInsets.symmetric(horizontal: 16), - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), - child: trailing == null && leading == null ? - Text(text, softWrap: true, style: textStyle) : - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - if (leading != null) leading, - Expanded(child: Text(text, style: textStyle)), - if (trailing != null) trailing - ], - ), + padding: padding ?? const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: trailing == null && leading == null + ? Text( + text, + softWrap: true, + style: textStyle ?? const TextStyle( + fontSize: 15, + color: Colors.white, + fontWeight: FontWeight.normal + ), + textAlign: textAlign + ) + : Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (leading != null) leading, + if (trailing != null) + Expanded(child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + text, + style: textStyle ?? const TextStyle( + fontSize: 15, + color: Colors.white, + fontWeight: FontWeight.normal + ), + textAlign: textAlign + ) + )) + else + Container( + constraints: BoxConstraints(maxWidth: _getMaxWidthTitleByLeadingTrailingSize( + currentMaxWidth: maxWidth, + leading: leading + )), + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + text, + style: textStyle ?? const TextStyle( + fontSize: 15, + color: Colors.white, + fontWeight: FontWeight.normal + ), + textAlign: textAlign + ), + ), + if (trailing != null) trailing + ], + ), ), ), ); - _overlayEntry = OverlayEntry( - builder: (BuildContext context) => - _showWidgetBasedOnPosition( - toastDuration != null ? - ToastCard( - child, - Duration(seconds: toastDuration), - fadeDuration: 500, - ) - : child, toastPosition, - )); + _overlayEntry = OverlayEntry(builder: (context) => + _showWidgetBasedOnPosition( + toastDuration != null + ? ToastCard(child, toastDuration) + : child, + toastPosition, + ) + ); _isVisible = true; overlayState!.insert(_overlayEntry!); if (toastDuration != null) { - await Future.delayed(Duration(seconds: toastDuration)); + await Future.delayed(toastDuration); await dismiss(); } } + static double _getMaxWidthTitleByLeadingTrailingSize({ + required double currentMaxWidth, + Widget? leading + }) { + return leading == null ? currentMaxWidth : currentMaxWidth - 100; + } + static Positioned _showWidgetBasedOnPosition( - Widget child, ToastPosition? toastPosition) { + Widget child, + ToastPosition? toastPosition + ) { switch (toastPosition) { case ToastPosition.BOTTOM: - return Positioned(bottom: 60, left: 18, right: 18, child: child); + return Positioned(bottom: 80, left: 18, right: 18, child: child); case ToastPosition.BOTTOM_LEFT: - return Positioned(bottom: 60, left: 18, child: child); + return Positioned(bottom: 80, left: 18, child: child); case ToastPosition.BOTTOM_RIGHT: - return Positioned(bottom: 60, right: 18, child: child); + return Positioned(bottom: 80, right: 18, child: child); case ToastPosition.CENTER: - return Positioned( - top: 60, bottom: 60, left: 18, right: 18, child: child); + return Positioned(top: 60, bottom: 60, left: 18, right: 18, child: child); case ToastPosition.CENTER_LEFT: return Positioned(top: 60, bottom: 60, left: 18, child: child); case ToastPosition.CENTER_RIGHT: @@ -165,9 +202,14 @@ class ToastView { } class ToastCard extends StatefulWidget { - const ToastCard(this.child, this.duration, - {Key? key, this.fadeDuration = 500}) - : super(key: key); + const ToastCard( + this.child, + this.duration, + { + Key? key, + this.fadeDuration = 300 + } + ) : super(key: key); final Widget child; final Duration duration; @@ -199,8 +241,7 @@ class ToastStateFulState extends State vsync: this, duration: Duration(milliseconds: widget.fadeDuration), ); - _fadeAnimation = - CurvedAnimation(parent: _animationController!, curve: Curves.easeIn); + _fadeAnimation = CurvedAnimation(parent: _animationController!, curve: Curves.easeIn); super.initState(); showAnimation(); @@ -226,4 +267,4 @@ class ToastStateFulState extends State opacity: _fadeAnimation as Animation, child: widget.child, ); -} +} \ No newline at end of file diff --git a/core/lib/utils/app_logger.dart b/core/lib/utils/app_logger.dart index 6c5bfbfc63..7d7f5a79f1 100644 --- a/core/lib/utils/app_logger.dart +++ b/core/lib/utils/app_logger.dart @@ -1,34 +1,76 @@ import 'dart:async'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -_Dispatcher logHistory = _Dispatcher(""); +final logHistory = _Dispatcher(''); -void log(String? value) { - String v = value ?? ""; - logHistory.value = v + "\n" + logHistory.value; - if (kReleaseMode == false) { - print(v); +void log(String? value, {Level level = Level.info}) { + if (!kDebugMode) return; + + String logsStr = value ?? ''; + logHistory.value = '$logsStr\n${logHistory.value}'; + + if (PlatformInfo.isWeb) { + switch (level) { + case Level.wtf: + logsStr = '\x1B[31m!!!CRITICAL!!! $logsStr\x1B[0m'; + break; + case Level.error: + logsStr = '\x1B[31m$logsStr\x1B[0m'; + break; + case Level.warning: + logsStr = '\x1B[33m$logsStr\x1B[0m'; + break; + case Level.info: + logsStr = '\x1B[32m$logsStr\x1B[0m'; + break; + case Level.debug: + logsStr = '\x1B[34m$logsStr\x1B[0m'; + break; + case Level.verbose: + break; + } + } else { + switch (level) { + case Level.error: + logsStr = '[ERROR] $logsStr'; + break; + default: + break; + } } + // ignore: avoid_print + print('[TeamMail] $logsStr'); } -void logError(String? value) => log("[ERROR] " + (value ?? "")); +void logError(String? value) => log(value, level: Level.error); // Take from: https://flutter.dev/docs/testing/errors void initLogger(VoidCallback runApp) { runZonedGuarded(() async { WidgetsFlutterBinding.ensureInitialized(); - FlutterError.onError = (FlutterErrorDetails details) { + FlutterError.onError = (details) { FlutterError.dumpErrorToConsole(details); - logError(details.stack.toString()); + logError('AppLogger::initLogger::runZonedGuarded:FlutterError.onError: ${details.stack.toString()}'); }; runApp.call(); - }, (Object error, StackTrace stack) { - logError(stack.toString()); + }, (error, stack) { + logError('AppLogger::initLogger::runZonedGuarded:onError: $error | stack: $stack'); }); } class _Dispatcher extends ValueNotifier { _Dispatcher(String value) : super(value); +} + + +enum Level { + wtf, + error, + warning, + info, + debug, + verbose, } \ No newline at end of file diff --git a/core/lib/utils/benchmark.dart b/core/lib/utils/benchmark.dart index 4af69cbab7..e3939856f1 100644 --- a/core/lib/utils/benchmark.dart +++ b/core/lib/utils/benchmark.dart @@ -33,4 +33,4 @@ class _Benchmark { } } -final _Benchmark bench = _Benchmark(); \ No newline at end of file +final bench = _Benchmark(); \ No newline at end of file diff --git a/core/lib/utils/build_utils.dart b/core/lib/utils/build_utils.dart index be54cc5e4c..2606727477 100644 --- a/core/lib/utils/build_utils.dart +++ b/core/lib/utils/build_utils.dart @@ -1,11 +1,10 @@ -import 'package:flutter/foundation.dart' as Foundation; -abstract class BuildUtils { - static const bool isDebugMode = Foundation.kDebugMode; +import 'package:flutter/foundation.dart'; - static const bool isReleaseMode = Foundation.kReleaseMode; +abstract class BuildUtils { + static const bool isDebugMode = kDebugMode; - static const bool isWeb = Foundation.kIsWeb; + static const bool isReleaseMode = kReleaseMode; - static const bool isProfileMode = Foundation.kProfileMode; + static const bool isProfileMode = kProfileMode; } \ No newline at end of file diff --git a/core/lib/utils/direction_utils.dart b/core/lib/utils/direction_utils.dart new file mode 100644 index 0000000000..058ea8c57b --- /dev/null +++ b/core/lib/utils/direction_utils.dart @@ -0,0 +1,16 @@ + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart' as intl; + +abstract class DirectionUtils { + + static bool isDirectionRTLByLanguage(BuildContext context) => intl.Bidi.isRtlLanguage(Localizations.localeOf(context).languageCode); + + static bool isDirectionRTLByEndsText(String text) => intl.Bidi.endsWithRtl(text); + + static bool isDirectionRTLByHasAnyRtl(String text) => intl.Bidi.hasAnyRtl(text); + + static TextDirection getDirectionByLanguage(BuildContext context) => isDirectionRTLByLanguage(context) ? TextDirection.rtl : TextDirection.ltr; + + static TextDirection getDirectionByEndsText(String text) => isDirectionRTLByEndsText(text) ? TextDirection.rtl : TextDirection.ltr; +} \ No newline at end of file diff --git a/core/lib/utils/file_utils.dart b/core/lib/utils/file_utils.dart new file mode 100644 index 0000000000..f521b68b4d --- /dev/null +++ b/core/lib/utils/file_utils.dart @@ -0,0 +1,112 @@ +import 'dart:io'; +import 'package:core/domain/exceptions/download_file_exception.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:path_provider/path_provider.dart'; + +class FileUtils { + + Future _getInternalStorageDirPath({ + required String nameFile, + String? folderPath, + String? extensionFile + }) async { + if (!PlatformInfo.isWeb) { + + String fileDirectory = (await getApplicationDocumentsDirectory()).absolute.path; + + if (folderPath != null) { + fileDirectory = '$fileDirectory/$folderPath'; + } + + Directory directory = Directory(fileDirectory); + + if (!await directory.exists()) { + await directory.create(recursive: true); + } + + fileDirectory = '$fileDirectory/$nameFile'; + + if (extensionFile != null) { + fileDirectory = '$fileDirectory.$extensionFile'; + } + + return fileDirectory; + } else { + throw DeviceNotSupportedException(); + } + } + + Future saveToFile({ + required String nameFile, + required String content, + String? folderPath, + String? extensionFile + }) async { + final internalStorageDirPath = await _getInternalStorageDirPath( + nameFile: nameFile, + folderPath: folderPath, + extensionFile: extensionFile + ); + + final file = File(internalStorageDirPath); + log("FileUtils()::saveToFile: $file"); + + return await file.writeAsString(content, mode: FileMode.write); + } + + Future deleteFile(String filePath) async { + final file = File(filePath); + if (await file.exists()) { + await file.delete(); + log("FileUtils()::deleteFile: $file"); + } else { + log("FileUtils()::deleteFile: File does not exist"); + } + } + + Future getContentFromFile({ + required String nameFile, + String? folderPath, + String? extensionFile + }) async { + final internalStorageDirPath = await _getInternalStorageDirPath( + nameFile: nameFile, + folderPath: folderPath, + extensionFile: extensionFile + ); + + final file = File(internalStorageDirPath); + final emailContent = await file.readAsString(); + + log("FileUtils()::getFromFile: $emailContent"); + + return emailContent; + } + + Future isFileExisted({ + required String nameFile, + String? folderPath, + String? extensionFile + }) async { + final internalStorageDirPath = await _getInternalStorageDirPath( + nameFile: nameFile, + folderPath: folderPath, + extensionFile: extensionFile + ); + + return File(internalStorageDirPath).exists(); + } + + void removeFolder(String folderName) async { + try { + String folderPath = (await getApplicationDocumentsDirectory()).absolute.path; + folderPath = '$folderPath/$folderName'; + log('FileUtils::removeFolder():folderPath: $folderPath'); + final dir = Directory(folderPath); + dir.deleteSync(recursive: true); + } catch (e) { + logError('FileUtils::removeFolder():EXCEPTION: $e'); + } + } +} \ No newline at end of file diff --git a/core/lib/utils/fps_manager.dart b/core/lib/utils/fps_manager.dart index 36e1cb4206..295ddefea5 100644 --- a/core/lib/utils/fps_manager.dart +++ b/core/lib/utils/fps_manager.dart @@ -34,8 +34,8 @@ class FpsManager { final List _fpsCallbacks = []; /// Temporarily save 120 frames - static const int _queue_capacity = 120; - final ListQueue framesQueue = ListQueue(_queue_capacity); + static const int queueCapacity = 120; + final ListQueue framesQueue = ListQueue(queueCapacity); void addFpsCallback(FpsCallback callback) { _fpsCallbacks.add(callback); @@ -69,7 +69,7 @@ class FpsManager { for (FrameTiming timing in timings) { framesQueue.addFirst(timing); } - while (framesQueue.length > _queue_capacity) { + while (framesQueue.length > queueCapacity) { framesQueue.removeLast(); } diff --git a/core/lib/utils/linkify_html.dart b/core/lib/utils/linkify_html.dart deleted file mode 100644 index b0653417a3..0000000000 --- a/core/lib/utils/linkify_html.dart +++ /dev/null @@ -1,64 +0,0 @@ - -import 'package:core/utils/app_logger.dart'; - -class LinkifyHtml { - - String generateLinkify(String inputText) { - var replacedText = _linkifyUrlAddress(inputText); - replacedText = _linkifyMailToAddress(replacedText); - return replacedText; - } - - RegExp _generateRegExp(String pattern) { - return RegExp(pattern, multiLine: true, caseSensitive: false); - } - - /// URLs starting with http://, https://, ftp://, or "www." without //. - String _linkifyUrlAddress(String inputText) { - var replacedText = inputText; - - // URLs starting with http://, https://, ftp:// - final regexLinkWithHttp = _generateRegExp(r'(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])'); - final newReplacedTextWithHttp = replacedText.replaceAllMapped(regexLinkWithHttp, (regexMatch) { - final link = regexMatch.group(1); - final newLinkWithHttp = link?.isNotEmpty == true - ? '$link' - : ''; - log('ConvertUrlStringToHtmlLinksTransformers::_linkifyUrlAddress(): newLinkWithHttp: $newLinkWithHttp'); - return newLinkWithHttp; - }); - replacedText = newReplacedTextWithHttp; - - // URLs starting with "www." without // before it or it'd re-link the ones done above. - final regexLinkWithWWW = _generateRegExp(r'(^|[^\/])(www\.[\S]+(\b|\$))'); - final newReplacedTextWithWWW = replacedText.replaceAllMapped(regexLinkWithWWW, (regexMatch) { - final previousChar = regexMatch.group(1); - log('LinkifyHtml::_linkifyUrlAddress(): previousChar: $previousChar'); - final link = regexMatch.group(2); - final newLinkWithWWW = link?.isNotEmpty == true - ? '$previousChar$link' - : ''; - log('ConvertUrlStringToHtmlLinksTransformers::_linkifyUrlAddress(): newLinkWithWWW: $newLinkWithWWW'); - return newLinkWithWWW; - }); - replacedText = newReplacedTextWithWWW; - - return replacedText; - } - - /// Change email addresses to mailto:: links. - String _linkifyMailToAddress(String inputText) { - final regexMailTo = _generateRegExp(r'(([a-zA-Z0-9\-\_\.])+@[a-zA-Z\_]+?(\.[a-zA-Z]{2,6})+)'); - - final newReplacedTextWitMailTo = inputText.replaceAllMapped(regexMailTo, (regexMatch) { - final emailAddress = regexMatch.group(1); - final newMailToLink = emailAddress?.isNotEmpty == true - ? '$emailAddress' - : ''; - log('ConvertUrlStringToHtmlLinksTransformers::_linkifyUrlAddress(): newMailToLink: $newMailToLink'); - return newMailToLink; - }); - - return newReplacedTextWitMailTo; - } -} \ No newline at end of file diff --git a/core/lib/utils/platform_info.dart b/core/lib/utils/platform_info.dart new file mode 100644 index 0000000000..be125ca08c --- /dev/null +++ b/core/lib/utils/platform_info.dart @@ -0,0 +1,14 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +abstract class PlatformInfo { + static const bool isWeb = kIsWeb; + static bool get isLinux => !kIsWeb && Platform.isLinux; + static bool get isWindows => !kIsWeb && Platform.isWindows; + static bool get isMacOS => !kIsWeb && Platform.isMacOS; + static bool get isIOS => !kIsWeb && Platform.isIOS; + static bool get isAndroid => !kIsWeb && Platform.isAndroid; + static bool get isMobile => isAndroid || isIOS; + static bool get isDesktop => isLinux || isWindows || isMacOS; +} \ No newline at end of file diff --git a/core/pubspec.lock b/core/pubspec.lock index b6bbd30cb8..8b538d217a 100644 --- a/core/pubspec.lock +++ b/core/pubspec.lock @@ -1,167 +1,166 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + args: + dependency: transitive + description: + name: args + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + url: "https://pub.dev" + source: hosted + version: "2.4.0" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" built_collection: dependency: "direct main" description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: "direct main" description: name: collection - url: "https://pub.dartlang.org" + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" source: hosted - version: "1.16.0" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.2" + version: "1.17.1" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted version: "1.0.5" dartz: dependency: "direct main" description: name: dartz - url: "https://pub.dartlang.org" + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" source: hosted version: "0.10.1" device_info_plus: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.2" - device_info_plus_linux: - dependency: transitive - description: - name: device_info_plus_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - device_info_plus_macos: - dependency: transitive - description: - name: device_info_plus_macos - url: "https://pub.dartlang.org" + sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" + url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "8.1.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.6.1" - device_info_plus_web: - dependency: transitive - description: - name: device_info_plus_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - device_info_plus_windows: - dependency: transitive - description: - name: device_info_plus_windows - url: "https://pub.dartlang.org" + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "7.0.0" dio: dependency: "direct main" description: name: dio - url: "https://pub.dartlang.org" + sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.0.0" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted version: "6.1.4" flex_color_picker: dependency: "direct main" description: name: flex_color_picker - url: "https://pub.dartlang.org" + sha256: f0e0db8e3e47435cfbe9aa15c71b898fa218be0fc4ae409e1e42d5d5266b2c90 + url: "https://pub.dev" + source: hosted + version: "3.2.0" + flex_seed_scheme: + dependency: transitive + description: + name: flex_seed_scheme + sha256: "7058288ef97d348657ac95cea25d65a9aac181ca08387ede891fd7230ad7600f" + url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "1.2.3" flutter: dependency: "direct main" description: flutter @@ -171,166 +170,300 @@ packages: dependency: "direct main" description: name: flutter_image_compress - url: "https://pub.dartlang.org" + sha256: "37f1b26399098e5f97b74c1483f534855e7dff68ead6ddaccf747029fb03f29f" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: "6d6c741ddba1dba5229d63ba75767064791a7ce845196b45e31105e93d67c949" + url: "https://pub.dev" + source: hosted + version: "6.0.0-beta.22" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "064a8ccbc76217dcd3b0fd6c6ea6f549e69b2849a0233b5bb46af9632c3ce2ff" + url: "https://pub.dev" source: hosted version: "1.1.0" flutter_keyboard_visibility: dependency: "direct main" description: name: flutter_keyboard_visibility - url: "https://pub.dartlang.org" + sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" + url: "https://pub.dev" + source: hosted + version: "5.4.1" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "1.0.0" flutter_keyboard_visibility_platform_interface: dependency: transitive description: name: flutter_keyboard_visibility_platform_interface - url: "https://pub.dartlang.org" + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" source: hosted version: "2.0.0" flutter_keyboard_visibility_web: dependency: transitive description: name: flutter_keyboard_visibility_web - url: "https://pub.dartlang.org" + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" source: hosted version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_svg: dependency: "direct main" description: name: flutter_svg - url: "https://pub.dartlang.org" + sha256: "97c5b291b4fd34ae4f55d6a4c05841d4d0ed94952e033c5a6529e1b47b4d2a29" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "2.0.2" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_typeahead: + dependency: "direct main" + description: + name: flutter_typeahead + sha256: f31211a8536f87908c3dcbdb88666e2f4d77f5f06c2b3a48eaad5599969ff32d + url: "https://pub.dev" + source: hosted + version: "4.6.0" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" - fluttertoast: - dependency: "direct main" - description: - name: fluttertoast - url: "https://pub.dartlang.org" - source: hosted - version: "8.0.8" get: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + url: "https://pub.dev" source: hosted version: "4.6.5" html: dependency: "direct main" description: name: html - url: "https://pub.dartlang.org" + sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.15.1" http: dependency: "direct main" description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted - version: "0.13.4" + version: "0.13.5" http_parser: - dependency: transitive + dependency: "direct main" description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + url: "https://pub.dev" + source: hosted + version: "0.18.0" js: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + linkify: + dependency: "direct main" + description: + name: linkify + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "5.0.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.15" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.9.1" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.1" - path_drawing: + version: "1.8.3" + path_parsing: dependency: transitive description: - name: path_drawing - url: "https://pub.dartlang.org" + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" source: hosted version: "1.0.1" - path_parsing: + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9" + url: "https://pub.dev" + source: hosted + version: "2.0.13" + path_provider_android: dependency: transitive description: - name: path_parsing - url: "https://pub.dartlang.org" + name: path_provider_android + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.27" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: ad4c4d011830462633f03eb34445a45345673dfd4faf1ab0b4735fbd93b19183 + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + url: "https://pub.dev" + source: hosted + version: "2.1.10" + path_provider_platform_interface: + dependency: "direct main" + description: + name: path_provider_platform_interface + sha256: c2af5a8a6369992d915f8933dfc23172071001359d17896e83db8be57db8a397 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 + url: "https://pub.dev" + source: hosted + version: "2.1.6" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" source: hosted - version: "5.0.0" - plugin_platform_interface: + version: "5.1.0" + platform: dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: "direct dev" description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: c384b19bf8d317b3d1576327203cdc95f96bf5f109ab63ab72690fe32fdb0d3c + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.0" pointer_interceptor: dependency: "direct main" description: name: pointer_interceptor - url: "https://pub.dartlang.org" + sha256: "49e6b86ba931d801ce852990d4a8913726ea3964266559e0b058baa3b4408435" + url: "https://pub.dev" source: hosted version: "0.9.1" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" sky_engine: dependency: transitive description: flutter @@ -340,191 +473,194 @@ packages: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" - sqflite: - dependency: "direct main" - description: - name: sqflite - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0+4" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - synchronized: - dependency: transitive - description: - name: synchronized - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "3.0.0+3" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.5.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" universal_html: dependency: "direct main" description: name: universal_html - url: "https://pub.dartlang.org" + sha256: b5061c64c7c863c12e46279e032976f1c274f927fb3589b52b5928dcd2d52f7c + url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.0.9" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.dartlang.org" + sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.0" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" + url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.1.10" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: dd729390aa936bf1bdf5cd1bc7468ff340263f80a2c4f569416507667de8e3c8 + url: "https://pub.dev" source: hosted - version: "6.0.19" + version: "6.0.26" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: "3dedc66ca3c0bef9e6a93c0999aee102556a450afcc1b7bcfeace7a424927d92" + url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.1.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.4" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.4" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.16" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd + url: "https://pub.dev" source: hosted - version: "3.0.1" - vector_math: + version: "3.0.5" + vector_graphics: dependency: transitive description: - name: vector_math - url: "https://pub.dartlang.org" + name: vector_graphics + sha256: "4cf8e60dbe4d3a693d37dff11255a172594c0793da542183cbfe7fe978ae4aaa" + url: "https://pub.dev" source: hosted - version: "2.1.2" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - webview_flutter_android: + version: "1.1.4" + vector_graphics_codec: dependency: transitive description: - name: webview_flutter_android - url: "https://pub.dartlang.org" + name: vector_graphics_codec + sha256: "278ad5f816f58b1967396d1f78ced470e3e58c9fe4b27010102c0a595c764468" + url: "https://pub.dev" source: hosted - version: "2.10.4" - webview_flutter_platform_interface: + version: "1.1.4" + vector_graphics_compiler: dependency: transitive description: - name: webview_flutter_platform_interface - url: "https://pub.dartlang.org" + name: vector_graphics_compiler + sha256: "0bf61ad56e6fd6688a2865d3ceaea396bc6a0a90ea0d7ad5049b1b76c09d6163" + url: "https://pub.dev" source: hosted - version: "1.9.5" - webview_flutter_wkwebview: + version: "1.1.4" + vector_math: dependency: transitive description: - name: webview_flutter_wkwebview - url: "https://pub.dartlang.org" + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.9.5" + version: "2.1.4" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + sha256: "5cdbe09a75b5f4517adf213c68aaf53ffa162fadf54ba16f663f94f3d2664a56" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "1.0.0" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.2" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.7.0" diff --git a/core/pubspec.yaml b/core/pubspec.yaml index 6a5bc4b9fb..f92967c708 100644 --- a/core/pubspec.yaml +++ b/core/pubspec.yaml @@ -20,70 +20,68 @@ publish_to: none version: 1.0.0+1 environment: - sdk: ">=2.16.2 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + ### Dependencies from pub.dev ### + cupertino_icons: 1.0.5 - # flutter_svg - flutter_svg: 1.1.0 + flutter_svg: 2.0.2 - # Http client - dio: 4.0.6 + dio: 5.0.0 - # dartz dartz: 0.10.1 - # equatable - equatable: 2.0.3 + equatable: 2.0.5 built_collection: 5.1.1 - html: 0.15.0 + html: 0.15.1 - # fluttertoast - fluttertoast: 8.0.8 + get: 4.6.5 - # sqflite - sqflite: 2.0.0+4 + device_info_plus: 8.1.0 - # GetX - get: 4.6.5 + flutter_inappwebview: 6.0.0-beta.22 - # device_info_plus - device_info_plus: 4.0.2 + url_launcher: 6.1.10 - # webview_flutter - webview_flutter: 3.0.0 + universal_html: 2.0.9 - # url_launcher - url_launcher: 6.1.5 + http: 0.13.5 - # collection - collection: 1.16.0 + pointer_interceptor: 0.9.1 - universal_html: 2.0.8 + flutter_keyboard_visibility: 5.4.1 - http: 0.13.4 + flex_color_picker: 3.2.0 - pointer_interceptor: 0.9.1 + flutter_image_compress: 1.1.3 + + http_parser: 4.0.2 - flutter_keyboard_visibility: 5.2.0 + path_provider: 2.0.13 - flex_color_picker: 2.5.0 + path_provider_platform_interface: 2.0.1 - flutter_image_compress: 1.1.0 + collection: 1.17.1 + + intl: 0.18.0 + + flutter_typeahead: 4.6.0 + + linkify: 5.0.0 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: 1.0.4 + flutter_lints: 2.0.1 + + plugin_platform_interface: 2.1.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/core/test/linkify_html_test.dart b/core/test/linkify_html_test.dart deleted file mode 100644 index 629f00ee09..0000000000 --- a/core/test/linkify_html_test.dart +++ /dev/null @@ -1,46 +0,0 @@ - -import 'package:core/utils/linkify_html.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('linkify_html test', () { - - final linkifyHtml = LinkifyHtml(); - - test( - 'generateLinkify should return url when input text contain http/https/ftp', - () async { - final htmlValidate = linkifyHtml.generateLinkify( - 'See at Hanoi' - ); - expect( - htmlValidate, - equals('See <https://linagora.com> at Hanoi')); - } - ); - - test( - 'generateLinkify should return www when input text contain www', - () async { - final htmlValidate = linkifyHtml.generateLinkify( - 'See www.google.com at Hanoi' - ); - expect( - htmlValidate, - equals('See www.google.com at Hanoi')); - } - ); - - test( - 'generateLinkify should return url when input text contain email address', - () async { - final htmlValidate = linkifyHtml.generateLinkify( - 'See tdvu@linagora.com at Hanoi' - ); - expect( - htmlValidate, - equals('See tdvu@linagora.com at Hanoi')); - } - ); - }); -} \ No newline at end of file diff --git a/core/test/presentation/extensions/uri_extension_test.dart b/core/test/presentation/extensions/uri_extension_test.dart new file mode 100644 index 0000000000..87e4921499 --- /dev/null +++ b/core/test/presentation/extensions/uri_extension_test.dart @@ -0,0 +1,59 @@ + +import 'package:core/presentation/extensions/uri_extension.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + + group('method convertToQualifiedUrl test', () { + + test('convertToQualifiedUrl() should return qualified url when baseUrl is `https://domain.com` and sourceUrl is `https://domain.com/jmap`', () async { + final baseUrl = Uri.parse('https://domain.com'); + final sourceUrl = Uri.parse('https://domain.com/jmap'); + + final qualifiedUrlExpected = Uri.parse('https://domain.com/jmap'); + final qualifiedUrlResult = sourceUrl.toQualifiedUrl(baseUrl: baseUrl); + + expect(qualifiedUrlResult, equals(qualifiedUrlExpected)); + }); + + test('convertToQualifiedUrl() should return qualified url when baseUrl is `https://domain.com` and sourceUrl is `/jmap/`', () async { + final baseUrl = Uri.parse('https://domain.com'); + final sourceUrl = Uri.parse('/jmap/'); + + final qualifiedUrlExpected = Uri.parse('https://domain.com/jmap'); + final qualifiedUrlResult = sourceUrl.toQualifiedUrl(baseUrl: baseUrl); + + expect(qualifiedUrlResult, equals(qualifiedUrlExpected)); + }); + + test('convertToQualifiedUrl() should return qualified url when baseUrl is `https://domain.com` and sourceUrl is `/jmap`', () async { + final baseUrl = Uri.parse('https://domain.com'); + final sourceUrl = Uri.parse('/jmap'); + + final qualifiedUrlExpected = Uri.parse('https://domain.com/jmap'); + final qualifiedUrlResult = sourceUrl.toQualifiedUrl(baseUrl: baseUrl); + + expect(qualifiedUrlResult, equals(qualifiedUrlExpected)); + }); + + test('convertToQualifiedUrl() should return qualified url when baseUrl is `https://domain.com/jmap` and sourceUrl is empty', () async { + final baseUrl = Uri.parse('https://domain.com/jmap'); + final sourceUrl = Uri.parse(''); + + final qualifiedUrlExpected = Uri.parse('https://domain.com/jmap'); + final qualifiedUrlResult = sourceUrl.toQualifiedUrl(baseUrl: baseUrl); + + expect(qualifiedUrlResult, equals(qualifiedUrlExpected)); + }); + + test('convertToQualifiedUrl() should return qualified url when baseUrl is `https://domain.com/jmap` and sourceUrl is `/`', () async { + final baseUrl = Uri.parse('https://domain.com/jmap'); + final sourceUrl = Uri.parse('/'); + + final qualifiedUrlExpected = Uri.parse('https://domain.com/jmap'); + final qualifiedUrlResult = sourceUrl.toQualifiedUrl(baseUrl: baseUrl); + + expect(qualifiedUrlResult, equals(qualifiedUrlExpected)); + }); + }); +} \ No newline at end of file diff --git a/core/test/sanitize_autolink_filter_test.dart b/core/test/sanitize_autolink_filter_test.dart new file mode 100644 index 0000000000..98fe740ded --- /dev/null +++ b/core/test/sanitize_autolink_filter_test.dart @@ -0,0 +1,55 @@ +import 'dart:convert'; + +import 'package:core/presentation/utils/html_transformer/sanitize_autolink_filter.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('SanitizeAutolinkFilter test', () { + + final sanitizeAutolinkFilter = SanitizeAutolinkFilter(const HtmlEscape()); + + test( + 'SanitizeAutolinkFilter should return html a tag with href="urlLink" when input text contain https', + () { + final htmlValidate = sanitizeAutolinkFilter.process('See https://linagora.com at Hanoi'); + expect( + htmlValidate, + equals('See linagora.com at Hanoi') + ); + } + ); + + test( + 'SanitizeAutolinkFilter should return html a tag with href="urlLink" when input text contain http', + () { + final htmlValidate = sanitizeAutolinkFilter.process('See http://linagora.com at Hanoi'); + expect( + htmlValidate, + equals('See linagora.com at Hanoi') + ); + } + ); + + test( + 'SanitizeAutolinkFilter should return html a tag with href="urlLink" when input text contain www', + () { + final htmlValidate = sanitizeAutolinkFilter.process('See www.linagora.com at Hanoi'); + expect( + htmlValidate, + equals('See linagora.com at Hanoi') + ); + } + ); + + test( + 'SanitizeAutolinkFilter should return html a tag with href="mailToLink" when input text contain email address', + () { + final htmlValidate = sanitizeAutolinkFilter.process('See tdvu@linagora.com at Hanoi'); + expect( + htmlValidate, + equals('See tdvu@linagora.com at Hanoi') + ); + } + ); + }); +} \ No newline at end of file diff --git a/core/test/utils/file_utils_test.dart b/core/test/utils/file_utils_test.dart new file mode 100644 index 0000000000..6596881b27 --- /dev/null +++ b/core/test/utils/file_utils_test.dart @@ -0,0 +1,51 @@ +import 'package:core/utils/file_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +const String kApplicationDocumentsPath = 'applicationDocumentsPath'; + +void main() { + const fileName = 'test_file'; + const fileContent = 'Hello, World!'; + + group('fileUtils test', () { + setUp(() async { + PathProviderPlatform.instance = FakePathProviderPlatform(); + }); + + test('Store private HTLM String to File', () async { + + final file = await FileUtils().saveToFile(nameFile: fileName, content: fileContent); + + expect(await file.exists(), equals(true)); + + expect(await file.readAsString(), equals(fileContent)); + + file.delete(); + }); + + test('Get HTML String from File Private', () async { + + /// Create a temporary file that will be deleted after `getFromFile` is done + final file = await FileUtils().saveToFile(nameFile: fileName, content: fileContent); + + final htmlString = await FileUtils().getContentFromFile(nameFile: fileName); + + expect(htmlString.isNotEmpty, equals(true)); + + expect(htmlString, equals(fileContent)); + + file.delete(); + }); + }); +} + +class FakePathProviderPlatform extends Fake + with MockPlatformInterfaceMixin + implements PathProviderPlatform { + @override + Future getApplicationDocumentsPath() async { + return kApplicationDocumentsPath; + } +} \ No newline at end of file diff --git a/docs/adr/0019-conventions-for-display-push-notifications.md b/docs/adr/0019-conventions-for-display-push-notifications.md new file mode 100644 index 0000000000..916efa7353 --- /dev/null +++ b/docs/adr/0019-conventions-for-display-push-notifications.md @@ -0,0 +1,22 @@ +# 19. Conventions for display push notifications + +Date: 2023-03-31 + +## Status + +Accepted + +## Context + +In response to user experience, push notifications should display correctly. + +## Decision + +Let's not put notifications if: + +- The email is in one of these mailbox: `Sent, Draft, Outbox, Trash, Spam` +- The email is `seen` + +## Consequences + +- Increase user experience. diff --git a/docs/adr/0020-fix-tmail-ui-broken-after-create-identity-successfully-by-html-style.md b/docs/adr/0020-fix-tmail-ui-broken-after-create-identity-successfully-by-html-style.md new file mode 100644 index 0000000000..c58f473db2 --- /dev/null +++ b/docs/adr/0020-fix-tmail-ui-broken-after-create-identity-successfully-by-html-style.md @@ -0,0 +1,19 @@ +# 20. Fix Team Mail UI is broken after user create identity successfully by html (#1688) + +Date: 2023-04-03 + +## Status + +- Issue: [#1688](https://github.com/linagora/tmail-flutter/issues/1688) + +## Context + +- Root cause: Flutter was unable to create enough overlay surfaces. This is usually caused by too many platform views being displayed at once. + +## Decision + +- Use `pointer_interceptor` version `v0.9.1` + +## Consequences + +- No more UI loss errors diff --git a/docs/adr/0021-manage-changelog-by-cider.md b/docs/adr/0021-manage-changelog-by-cider.md new file mode 100644 index 0000000000..cb9a686e2b --- /dev/null +++ b/docs/adr/0021-manage-changelog-by-cider.md @@ -0,0 +1,29 @@ +# 21. Manage Changelog by Cider + +Date: 2023-04-05 + +## Status + +Accepted + +## Context + +- We did not manage Changedlog with the standard way + +## Decision + +Use Cider (https://pub.dev/packages/cider) to easier to manage changelog to follow [Changelog](https://keepachangelog.com/en/1.1.0/) format + +## Consequences + +- We should add to the change log some `type` of change: `added`, `changed`, `deprecated`, `removed`, `fixed`, `security` in each PR with command +``` +cider log +``` + +- In release phase, change the version in `pubspec.yaml` and dont forget to change the tag in cider template in the same file + +- Then run: +``` +cider release +``` \ No newline at end of file diff --git a/docs/adr/0022-fix-double-vertical-scroll-bar-on-composer.md b/docs/adr/0022-fix-double-vertical-scroll-bar-on-composer.md new file mode 100644 index 0000000000..ef40fde569 --- /dev/null +++ b/docs/adr/0022-fix-double-vertical-scroll-bar-on-composer.md @@ -0,0 +1,26 @@ +# 22. Fix double vertical scroll bar when hide status bar + +Date: 2023-04-21 + +## Status + +- Issue: [#1740](https://github.com/linagora/tmail-flutter/issues/1740) + +## Context + +- Root cause: + - When we using `html_editor_enhanced` lib can't auto resize `HTML Iframe` when changing the size of the screen + - `.note-editor.note-airframe .note-editing-area .note-editable,.note-editor.note-frame .note-editing-area .note-editable` has an overridden padding under the widgets + - When enabling `Code View` modem has an overridden padding under the widgets. Can't see break line + +## Decision + +- Use `setFullScreen` to `HTML Iframe` for the purpose of automatically resizing when there are changes +- Remove padding of `.note-editor.note-airframe .note-editing-area .note-editable,.note-editor.note-frame .note-editing-area .note-editable` +- Enable resize editor `disableResizeEditor: false` +- Remove padding of `.note-editor.note-airframe .note-editing-area .note-codable,.note-editor.note-frame .note-editing-area .note-codable` when enabling `Code View` mode + +## Consequences + +- No longer appear two vertical scroll bars of content on the editor +- Automatically resize `HTML Iframe` for dimensions \ No newline at end of file diff --git a/docs/adr/0023-fix-system-not-display-signature-in-email-which-has-been-sent.md b/docs/adr/0023-fix-system-not-display-signature-in-email-which-has-been-sent.md new file mode 100644 index 0000000000..e5792b29a8 --- /dev/null +++ b/docs/adr/0023-fix-system-not-display-signature-in-email-which-has-been-sent.md @@ -0,0 +1,19 @@ +# 23. Fix system not display signature in email which has been sent + +Date: 2023-04-21 + +## Status + +- Issue: [#1778](https://github.com/linagora/tmail-flutter/issues/1778) + +## Context + +- Root cause: Due to a syntax error in javascript. In string contains the characters `'` and `"` + +## Decision + +- Use `template literals` to escape a string in JavaScript. Follow on [enough_html_editor#20](https://github.com/linagora/enough_html_editor/pull/20) + +## Consequences + +- Escape a string in JavaScript avoid signature display error when sending email on mobile. \ No newline at end of file diff --git a/docs/adr/0024-fix-logic-of-replacing-dot-in-long-email-address.md b/docs/adr/0024-fix-logic-of-replacing-dot-in-long-email-address.md new file mode 100644 index 0000000000..435b43fefe --- /dev/null +++ b/docs/adr/0024-fix-logic-of-replacing-dot-in-long-email-address.md @@ -0,0 +1,20 @@ +# 24. Fix logic of replacing dot in long email address + +Date: 2023-04-21 + +## Status + +- Issue: [#1779](https://github.com/linagora/tmail-flutter/issues/1779) + +## Context + +- Root cause: When we use the `overflow=TextOverflow.ellipsis` property for the `Text` widget for long texts, it will result in incorrect string breaks. Since string contains some characters that are supposed to be word breaks in the string. The characters `space` and `-` + +## Decision + +- Convert those special characters to unicode. +- Flutter is working on fixing that bug and is expected to be updated in version `3.10`. Follow on [flutter#18761](https://github.com/flutter/flutter/issues/18761) + +## Consequences + +- Text overflow with ellipsis worked correctly diff --git a/docs/adr/0025-fix-can-not-select-suggestion-email-by-mouse-clicking-on-web.md b/docs/adr/0025-fix-can-not-select-suggestion-email-by-mouse-clicking-on-web.md new file mode 100644 index 0000000000..3bc962fe71 --- /dev/null +++ b/docs/adr/0025-fix-can-not-select-suggestion-email-by-mouse-clicking-on-web.md @@ -0,0 +1,24 @@ +# 25. Fix can't select suggestion email by mouse clicking on web + +Date: 2023-04-24 + +## Status + +- Issue: [#1576](https://github.com/linagora/tmail-flutter/issues/1576) +- Issue: [#1753](https://github.com/linagora/tmail-flutter/issues/1753) + +## Context + +Tap events are not being simulated to overlay on the `Web/Desktop` but work fine on mobile devices. This worked fine until the previous release stable `3.3` + +## Root cause + +Because the thing that the overlay is attached to is a `TextField`, so in order to keep from unfocused the text field when tapping outside of it, you need to tell the overlay widget that it's part of the TextField for purposes of the `Tap outside` behavior + +## Decision + +- Try wrapping a `TextFieldTapRegion` around the `Material` in the overlay. So that when the tap arrives, it's considered `inside` of the text field. + +## Consequences + +- Widget on overlay work fine. diff --git a/docs/adr/0026-fix-horizontal-scroll-bar-in-email-detail-view.md b/docs/adr/0026-fix-horizontal-scroll-bar-in-email-detail-view.md new file mode 100644 index 0000000000..41e8f784d5 --- /dev/null +++ b/docs/adr/0026-fix-horizontal-scroll-bar-in-email-detail-view.md @@ -0,0 +1,24 @@ +# 26. Hide Horizontal scroll bar in Email detail view + +Date: 2023-04-24 + +## Status + +- Issue: [#1743](https://github.com/linagora/tmail-flutter/issues/1743) + +## Context + +In email view always show horizontal scroll bar in Email detail view and don't wrap text instead + +## Root cause + +The horizontal scrollbar appears when the content inside the body element is wider than the width of the viewport. By default, the overflow property is set to "visible", which means that the content can overflow the element's boundaries. + +## Decision + +When you set `overflow-x: hidden`; on the body element, it prevents the content from overflowing the element's boundaries in the horizontal direction, +effectively disabling the horizontal scrollbar. However, if you don't set this property and the content is wider than the viewport, +the scrollbar will appear to allow the user to scroll horizontally to see the content that is outside the viewport. +## Consequences + +- Hide Horizontal scroll bar in Email detail view diff --git a/docs/adr/0027-use-tuplekey-store-data-cache.md b/docs/adr/0027-use-tuplekey-store-data-cache.md new file mode 100644 index 0000000000..86924533ff --- /dev/null +++ b/docs/adr/0027-use-tuplekey-store-data-cache.md @@ -0,0 +1,21 @@ +# 27. Use TupleKey store data cache + +Date: 2023-04-27 + +## Status + +Accepted + +## Context + +- Multiple accounts login at the same time in the same browser. The accounts will use the same database (`IndexDatabase`). + +## Decision + +- Use unique parameters (`AccountId`, `UserName`, `ObjectId(MailboxId/EmailId/StateType`) to form a unique `key` for storage (called `TupleKey`). +- TupleKey has the format: `ObjectId | AccountId | User`; +- `HiveDatabase` includes many `Box`. Each box is a `Map` with `key=TupleKey`. + +## Consequences + +- The correct `mailbox` and `email` lists are obtained for each account diff --git a/docs/adr/0028-use-work-manager-to-mange-task-scheduling.md b/docs/adr/0028-use-work-manager-to-mange-task-scheduling.md new file mode 100644 index 0000000000..afca1fbd50 --- /dev/null +++ b/docs/adr/0028-use-work-manager-to-mange-task-scheduling.md @@ -0,0 +1,94 @@ +# 28. Use WorkManager to manage task scheduling + +Date: 2023-05-23 + +## Status + +Accepted + +## Context + +- To automatically manage and schedule tasks. +- For example, automatically add email sending when network outage to `Queue` and automatically sending when network is back. + +## Decision + +- Use library `workmanager` on [pub.dev](https://pub.dev/packages/workmanager) + +### 1. Configuration + +- Add to `pubspec.yaml` +``` +dependencies: + workmanager: ^0.5.1 +``` + +- Setup on Android: [ANDROID_SETUP.md](https://github.com/fluttercommunity/flutter_workmanager/blob/main/ANDROID_SETUP.md) + +- Setup on iOS: [IOS_SETUP.md](https://github.com/fluttercommunity/flutter_workmanager/blob/main/IOS_SETUP.md) + +### 2. Implement + +- Initialize the WorkManager + +``` +await WorkManagerConfig().initialize(); +``` + +- Create new `Worker` +``` +final worker = Worker( + id: String // Task identifier + type: String // Job type, used to group jobs with the same purpose (Like Insert, Delete, Sent) + data: Map // Input parameters. Valid value types are: int, bool, double, String and their list +); +``` + +- Create new `WorkRequest` + ++ For non-repeating work: +``` +final workReuest = OneTimeWorkRequest( + worker + Duration? initialDelay, + Duration? backoffPolicyDelay, + ExistingWorkPolicy? existingWorkPolicy, + BackoffPolicy? backoffPolicy, + OutOfQuotaPolicy? outOfQuotaPolicy, + Constraints? constraints +); +``` ++ For repeating work: +``` +final workReuest = PeriodicWorkRequest( + worker + Duration? frequency, + Duration? initialDelay, + Duration? backoffPolicyDelay, + ExistingWorkPolicy? existingWorkPolicy, + BackoffPolicy? backoffPolicy, + OutOfQuotaPolicy? outOfQuotaPolicy, + Constraints? constraints +); +``` + +- Add to `enqueue` + +``` +await WorkSchedulerController().enqueue(workReuest); +``` + +- Handle task in `handleBackgroundTask` method +- To cancel task in WorkManager +``` +await WorkSchedulerController().cancelByWorkType(workType); + +await WorkSchedulerController().cancelByUniqueId(uniqueId); + +await WorkSchedulerController().cancelAll(); +``` + + +## Consequences + +- The correct `mailbox` and `email` lists are obtained for each account diff --git a/docs/adr/0029-fix-show-widget-of-draggable-when-click-right-on-email.md b/docs/adr/0029-fix-show-widget-of-draggable-when-click-right-on-email.md new file mode 100644 index 0000000000..6a0c34ed6a --- /dev/null +++ b/docs/adr/0029-fix-show-widget-of-draggable-when-click-right-on-email.md @@ -0,0 +1,49 @@ +# 29. Fix show widget of Draggable when click right on email + +Date: 2023-05-24 + +## Status + +- Issue: [#1743](https://github.com/linagora/tmail-flutter/issues/1862) + +## Context + +When user click right on email, the feedback widget of Draggable will show. + +## Root cause + +The childWhenDragging widget will still be draggable, including the ability to trigger the right-click event. +This is because the childWhenDragging widget is a part of the draggable behavior and inherits the drag-related properties and behaviors from the Draggable widget. +The Draggable widget allows you to customize the appearance and behavior of the dragged item during the dragging process by providing the feedback and childWhenDragging properties. +The feedback widget is used to display a visual representation of the dragged item, while the childWhenDragging widget is used to replace the original item when it is being dragged. + + +## Decision + +Wrap it with a GestureDetector and handle the right-click event as mentioned in the previous answer. This way, you can have separate behavior for the original item and the item being dragged. + +``` + GestureDetector( + behavior: HitTestBehavior.translucent, + onSecondaryTapDown: (details) { + // 1. Use empty callback to disable D&D on mouse right button + // 2. Call `showMenu` to show context menu + }, + child: Draggable>( + data: controller.listEmailDrag, + feedback: _buildFeedBackWidget(context), + childWhenDragging: _buildEmailItemWhenDragging(context, presentationEmail), + dragAnchorStrategy: pointerDragAnchorStrategy, + onDragStarted: () { + controller.calculateDragValue(presentationEmail); + controller.onDragMailBox(true); + }, + onDragEnd: (_) => controller.onDragMailBox(false), + onDraggableCanceled: (_,__) => controller.onDragMailBox(false), + child: _buildEmailItemNotDraggable(context, presentationEmail) + ), + ); +``` +## Consequences + +- Do not show the move dialog at all when I right click diff --git a/docs/adr/0030-fix-can-not-see-link-when-type-is-text-plain-in-email-view.md b/docs/adr/0030-fix-can-not-see-link-when-type-is-text-plain-in-email-view.md new file mode 100644 index 0000000000..a378037ce1 --- /dev/null +++ b/docs/adr/0030-fix-can-not-see-link-when-type-is-text-plain-in-email-view.md @@ -0,0 +1,39 @@ +# 30. Fix can not see link when type is text/plain in email view + +Date: 2023-08-01 + +## Status + +- Issue: + +[1881](https://github.com/linagora/tmail-flutter/issues/1881) +[2047](https://github.com/linagora/tmail-flutter/issues/2047) + +## Context + +- When `Content-Type=Text/Plain` the `url` and `email-addreess` links are not automatically `highlighted`. +- Some `url` links are `highlighted` when clicking, opening the content in the email view itself does not create a new tab. + +## Root cause + +- Because we perform `html escape` before creating `autolink`, it makes lost tags undetectable. Moreover, the detect function is ignored in some cases + +## Decision + +- Use linkify to detect `url/email/text` and return a list of corresponding elements. +```dart + final elements = linkify(inputText); +``` + +- If element is `TextElement` we perform html escape for it. +- If element is `UrlElement` or `EmailElement` we make html link tag (``) for it. +```dart + String _buildLinkTag({required String link, required String value}) { + return '$value'; + } +``` + +## Consequences + +- Accurately detect and fully highlight `url` and `email-address` links in email body when `Content-Type=Text/plain` +- Automatically open new tab when clicking on `url` link and open composer when clicking on `email-address` diff --git a/docs/adr/0031-fix-refresh-token-with-oidc.md b/docs/adr/0031-fix-refresh-token-with-oidc.md new file mode 100644 index 0000000000..3e28cf293b --- /dev/null +++ b/docs/adr/0031-fix-refresh-token-with-oidc.md @@ -0,0 +1,29 @@ +# 30. Fix refresh token with OIDC using `QueuedInterceptor` + +Date: 2023-09-11 + +## Status + +- Issue: + +[1974](https://github.com/linagora/tmail-flutter/issues/1974) + +## Context + +- Requests still return `401` after retrieving a new token. The application automatically logs out + +## Root cause + +- When executing tasks concurrently, they are pushed into the `Queue` along with the old `header` values (the old authentication is retained). +So when the first request receives a `401` error and tries to get a new token, it will be updated with the new authentication header value. + +## Decision + +- Use `QueuedInterceptor` to serialize `requests/responses/errors` before they enter the interceptor. +If there are multiple concurrent requests, the request is added to a queue before entering the interceptor. +Only one request at a time enters the interceptor, and after that request is processed by the interceptor, the next request will enter the interceptor. +- Try to make a `retry` request up to 3 times when receiving a `401` error. Aims to update the new token value on `memmory` to requests in the `queue`. + +## Consequences + +- The following `requests` were completed successfully. The application is not automatically logged out diff --git a/docs/configuration/app_grid_configuration.md b/docs/configuration/app_grid_configuration.md index aa29d7f0a5..abd5a58c99 100644 --- a/docs/configuration/app_grid_configuration.md +++ b/docs/configuration/app_grid_configuration.md @@ -13,7 +13,10 @@ { "appName": "Contacts", "icon": "ic_contacts_app.svg", - "appLink": "https://openpaas.linagora.com/contacts/" + "appLink": "https://openpaas.linagora.com/contacts/", + "androidPackageId": "xxx", + "iosUrlScheme": "xxx", + "iosAppStoreLink": "xxx" } ``` @@ -25,12 +28,18 @@ For example: { "appName": "Twake", "icon": "ic_twake_app.svg", - "appLink": "http://twake.linagora.com/" + "appLink": "http://twake.linagora.com/", + "androidPackageId": "xxx", + "iosUrlScheme": "xxx", + "iosAppStoreLink": "xxx" }, { "appName": "App 1", "icon": "ic_twake_app.svg", - "appLink": "http://twake.linagora.com/" + "appLink": "http://twake.linagora.com/", + "androidPackageId": "xxx", + "iosUrlScheme": "xxx", + "iosAppStoreLink": "xxx" }, ... ] @@ -40,9 +49,30 @@ For example: - `appName`: The name will be showed in App Grid - `icon`: Name of icon was added in `configurations\icons` folder - `appLink`: Service URL +- `androidPackageId`: ApplicationId of android app +- `iosUrlScheme`: UrlScheme name of the ios app. +- `iosAppStoreLink`: iTunes link of the ios app. +Allow navigate to store (appStore) if app is not found in the device. Example: +`itms-apps://itunes.apple.com/us/app/linshare/id1534003175` or `https://itunes.apple.com/us/app/linshare/id1534003175` 2. Enable it in [env.file](https://github.com/linagora/tmail-flutter/blob/master/env.file) ``` APP_GRID_AVAILABLE=supported ``` If you want to disable it, please change the value to `unsupported` or remove this from `env.file` + +3. Enable open app on mobile(Android/iOS) + +- In `Android 11+` to open another app already installed, you have to add the package with name `ApplicationId`' in [AndroidManifest](https://github.com/linagora/tmail-flutter/blob/master/android/app/src/main/AndroidManifest.xml) inside `queries` tag. Example: +``` + + + +``` +- - In `iOS 9+` to open another app already installed, you have to add the `UrlScheme`' in [Info.plist](https://github.com/GeekyAnts/external_app_launcher/blob/master/example/ios/Runner/Info.plist) under the `LSApplicationQueriesSchemes` key. Example: +``` +LSApplicationQueriesSchemes + + linshare.mobile + +``` diff --git a/docs/configuration/oidc_configuration.md b/docs/configuration/oidc_configuration.md new file mode 100644 index 0000000000..1dcfc80631 --- /dev/null +++ b/docs/configuration/oidc_configuration.md @@ -0,0 +1,45 @@ +## Configuration for OIDC + +### Context +- Team Mail ready to work with OIDC both for Mobile and Web version + +### How to config + +- OIDC_SCOPES: Scopes of OIDC application, each scope is separated by a comma `,` +- Other configurations depend on the platform: + +#### Web: +- `DOMAIN_REDIRECT_URL`: URL of your TeamMail web application +- `WEB_OIDC_CLIENT_ID`: Client ID of your OIDC application + +#### Mobile: +- if you want to change client Id for mobile + +For Android: +- `/android/app/build.gradle` + +```gradle + manifestPlaceholders = [ + 'appAuthRedirectScheme': 'teammail.mobile' + ] +``` + +- `model/lib/oidc/oidc_configuration.dart` + +```dart + static const String redirectUrlScheme = 'teammail.mobile'; +``` + +For iOS: +- `/ios/Runner/Info.plist` + +```plist + + + CFBundleURLSchemes + + teammail.mobile + + + +``` \ No newline at end of file diff --git a/docs/stories/EPIC-Label/TF-1639-user-can-manage-labels.md b/docs/stories/EPIC-Label/TF-1639-user-can-manage-labels.md new file mode 100644 index 0000000000..f3c855b732 --- /dev/null +++ b/docs/stories/EPIC-Label/TF-1639-user-can-manage-labels.md @@ -0,0 +1,72 @@ +# Summary + +* [Related EPIC](#related-epic) +* [Definition](#definition) +* [Screenshots](#screenshots) +* [Misc](#misc) + +## Related EPIC + +https://github.com/linagora/tmail-flutter/issues/1638 + +## Definition +### UC1. View label list in the left panel +- Given that I am a Tmail user and I have logged in successfully +- On the left panel, I can see a new category: Label +- Under this category, I can see list of label that I have created: + - Each label contain a tag icon with color, and label’s name +- If currently there is no label, there will be a text: No label under category’s name + +![image](https://user-images.githubusercontent.com/68209176/226865639-544eb439-a3a4-4d7c-9401-4d3ea026d8f8.png) + +![image](https://user-images.githubusercontent.com/68209176/226865809-ee148cc0-8ea5-4f8d-8023-b963e5112ea4.png) + + +#### UC2. Create a new label +- Next to category “Label” in the left panel, I can see a Create button +- I click on this button, there will be a popup displayed. I need to fill in: + - Field “Name: Input label’s name. This is mandatory field and the max length of label name is as same as max length of mailbox’s name +- Color: I can select a color for the label from a color palette. Default is no color. +- After inputting name and selecting the color for the new label, I click button Save, the system will validate: + - If the field “Name’ is blank, there will be an error message: “This field is required.” + - If there is existing label with the same name, there will be an toast message: “ A label with this name already exists” +- If there is no error, new label will be created and it appears in label list in left panel with selected color. + + +![image](https://user-images.githubusercontent.com/68209176/226866143-e99884e7-db1e-46e6-918b-b1b539e7cea0.png) + +![image](https://user-images.githubusercontent.com/68209176/226866188-a40d43d6-460f-4972-85be-114b69ceb9d7.png) + +![image](https://user-images.githubusercontent.com/68209176/227886707-d7319f9d-4d0e-4ed1-a834-9ef9a19320d7.png) + + + +#### UC3. Edit a label +- I click on three-dot button of a label in left panel, a list of actions is shown: Open in new tab, Edit and Delete +- I select Edit, the popup of label will be displayed: +- The popup is as same as popup when I create the label +- Name of label is displayed but I cannot edit this name +- I can only select another color for this label then click button Save +- The new color is update for the label and all the messages that have this label will be updated with new color too. + +![image](https://user-images.githubusercontent.com/68209176/227483148-6d3edee7-7b10-4248-a12c-724bbc4c5a14.png) + + +![image](https://user-images.githubusercontent.com/68209176/226866412-3d5aca75-e290-4bb4-a8b5-676577571ef0.png) + + + + +[Back to Summary](#summary) + +## Screenshots + +None + +[Back to Summary](#summary) + +## Misc + +None + +[Back to Summary](#summary) diff --git a/docs/stories/EPIC-Label/TF-1640-User-can-assign-label-to-an-email.md b/docs/stories/EPIC-Label/TF-1640-User-can-assign-label-to-an-email.md new file mode 100644 index 0000000000..44bb519506 --- /dev/null +++ b/docs/stories/EPIC-Label/TF-1640-User-can-assign-label-to-an-email.md @@ -0,0 +1,64 @@ +# Summary + +* [Related EPIC](#related-epic) +* [Definition](#definition) +* [Screenshots](#screenshots) +* [Misc](#misc) + +## Related EPIC + +https://github.com/linagora/tmail-flutter/issues/1638 + +## Definition + +**UC1. Assign a label for messages in thread-view** + +- Given that I am a Tmail user and I have logged in successfully +- On thread-view, I select one or multiple messages +- On action bar, I can see an icon Label +- I click on this icon, there will be a popup that listing all current labels +- I can select one or multiple label then click button Apply +- Then selected labels will be appeared on message as tags, on both thread-view and message content view + +![image](https://user-images.githubusercontent.com/68209176/228160592-91c5428e-0e36-4edc-a589-ea474408c0e1.png) + +**UC2. Assign a label for messages in message detail view** + +- Given that I am a Tmail user and I have logged in successfully +- I click on one message and the content will be opened +- On action list on top of screen, I can see an icon Label +- I click on this icon, there will be a popup that listing all current labels +- I can select one or multiple label then click button Apply +- Then selected labels will be appeared on this message as tags, on both thread-view and message content view + +![image](https://user-images.githubusercontent.com/68209176/228160873-4046f365-8693-45a1-b088-38b248ddb964.png) + + +**UC3. Remove a label on a message** + +- Given that I am a Tmail user and I have logged in successfully +- On thread-view I click on one message that currently have some label, the content view of this message is opened. +- On content view, I can see labels of this emaill, each label contains a cross icon +- I click on Cross icon, +- The label is removed from this message and there will be a toast notification +“[Label name] is removed from this email]” and Undo button +- If I click Undo, the label is back to this email again. + +![image](https://user-images.githubusercontent.com/68209176/228167740-50be89b5-f3d4-44b3-8f22-012848ade474.png) + +![image](https://user-images.githubusercontent.com/68209176/228920266-5f398a0b-45bf-44ef-904a-aa3016c53bc3.png) + + +[Back to Summary](#summary) + +## Screenshots + +None + +[Back to Summary](#summary) + +## Misc + +None + +[Back to Summary](#summary) diff --git a/docs/stories/EPIC-Label/TF-1641-user-can-view-all-messages-of-one-label.md b/docs/stories/EPIC-Label/TF-1641-user-can-view-all-messages-of-one-label.md new file mode 100644 index 0000000000..49d0337d53 --- /dev/null +++ b/docs/stories/EPIC-Label/TF-1641-user-can-view-all-messages-of-one-label.md @@ -0,0 +1,50 @@ +# Summary + +* [Related EPIC](#related-epic) +* [Definition](#definition) +* [Screenshots](#screenshots) +* [Misc](#misc) + +## Related EPIC + +https://github.com/linagora/tmail-flutter/issues/1638 + +## Definition +**UC1. Web browser** +- Given that I am a Tmail user and I have logged in successfully +- I click on one label in left panel, then the screen that listing all emails which have this label will be shown +- Or in the content view of an email, when I hover the label, there wull be a tooltip saying :"Go to label" +- I click on the label, then the screen listing all emails which have this label is displayed. +- On this email list, I can see the name of original mailbox of each email on the list + +**UC2. Mobile** + +- Given that I am a Tmail user and I have logged in successfully +- When I click on the folder name in top of screen, the list of folders screen is opened, I can see a label list +- I click on one label in the list, then the sytem will display all emails which have this label on the screen +- The screen's title is changed to selected label. + + +[Back to Summary](#summary) + +## Screenshots + +![image](https://user-images.githubusercontent.com/68209176/228921162-5033893b-60fa-4ee0-bc8f-d0503b46617f.png) + + +![image](https://user-images.githubusercontent.com/68209176/228170226-dafddf6c-21bd-4ff5-a2fc-055d93de8fb0.png) + +### Mobile + + +![Group 768](https://user-images.githubusercontent.com/68209176/231079344-c8329179-6b56-49df-9dce-3bd2b646b70f.png) ![Group 769 (1)](https://user-images.githubusercontent.com/68209176/231079508-ef71c541-ed12-4b74-8bb9-8ca0f2cefdee.png) + + + +[Back to Summary](#summary) + +## Misc + +None + +[Back to Summary](#summary) diff --git a/docs/stories/EPIC-Label/TF-1641-user-have-search-option-with-label.md b/docs/stories/EPIC-Label/TF-1641-user-have-search-option-with-label.md new file mode 100644 index 0000000000..b6ec7eacf0 --- /dev/null +++ b/docs/stories/EPIC-Label/TF-1641-user-have-search-option-with-label.md @@ -0,0 +1,54 @@ +# Summary + +* [Related EPIC](#related-epic) +* [Definition](#definition) +* [Screenshots](#screenshots) +* [Misc](#misc) + +## Related EPIC + +https://github.com/linagora/tmail-flutter/issues/1638 + +## Definition + +- Given that I am a Tmail user and I have logged in successfully +- I click on Search bar => select advanced option +- There will be a new search field: Label +- When I select this field,a popup is displayed +- On this popup, I can see the list of all current labels +- I can select one label or multipe label then click button Apply +- The selected labels will be displayed as tags on the search field Label, and in each tag there will be a cross icon to remove tag +- When I click button Search, the system will search all the mails that contains matched labels. +- In case Search multiple labels, "and" operator will be applied + - For example: If I select 2 labels: A and B in field Lable, the system will search for all emails that have both Lable A and Label B + + +[Back to Summary](#summary) + +## Screenshots +**Web verion** + +![image](https://user-images.githubusercontent.com/68209176/228719041-783547d2-e09b-47a3-a1e2-7ee91fe064f9.png) + +![image](https://user-images.githubusercontent.com/68209176/228719069-fc907834-695b-4339-b4d7-e7268709fb9a.png) + +![image](https://user-images.githubusercontent.com/68209176/228719153-987c965a-45b1-49d4-8530-4401afd25be9.png) + +**Mobile version** + +![image](https://user-images.githubusercontent.com/68209176/229978829-970506cf-fda3-45ef-bb70-3aafdd8827de.png) + +![Group 595](https://user-images.githubusercontent.com/68209176/229979079-dbf1c53d-cc9c-432a-8679-e87fd8835ea0.png) + +![Group 597 (1)](https://user-images.githubusercontent.com/68209176/229979269-13ea825c-ab17-4cf2-aea2-56fca763349d.png) + + + + +[Back to Summary](#summary) + +## Misc + +None + +[Back to Summary](#summary) diff --git a/docs/stories/EPIC-Restore deleted messages/TF-1719-User-can-restore-deleted-messages.md b/docs/stories/EPIC-Restore deleted messages/TF-1719-User-can-restore-deleted-messages.md new file mode 100644 index 0000000000..2c22631081 --- /dev/null +++ b/docs/stories/EPIC-Restore deleted messages/TF-1719-User-can-restore-deleted-messages.md @@ -0,0 +1,57 @@ +# Summary + +* [Related EPIC](#related-epic) +* [Definition](#definition) +* [Screenshots](#screenshots) +* [Misc](#misc) + +## Related EPIC +https://github.com/linagora/tmail-flutter/issues/1719 + +## Definition + +- Given that I am a Tmail user and I have logged in successfully +- When I click on three-dot button of folder Trash, I can see a new option in context menu: Recover deleted messages +- I select this option, a popup will be opened: +- In the popup, I can input fields: + - Deletion date: + + Default option is "Last 1 year" + + I can select other options: Last 7 days, Last 30 days, Last 6 months, Custom range + + When I select Custom range, a Date picker will be displayed that allow me to select From date and To date. + + The date range is limited between today to one year ago. + - Reception date: + + Default option is "All time" + + I can select other options: Last 7 days, Last 30 days, Last 6 months, Last year, Custom range + + When I select Custom range, a Date picker will be displayed that allow me to select From date and To date. + + The date range is not limited + - Subject: a text field + - Recipients: I can input name/email address, the system will display a suggestion list of matched emails. I can select from the list or input the recipients mannually. I can input multiple recipients. + - Sender: I can input name/email address, the system will display a suggestion list of matched emails. I can select from the list or input the sender mannually. I can input only 1 sender. + - Has attachment: a Checkbox. If it is checked, the system will find and restore email which contains attachments +- After input conditions, I click button Restore, the system will search for matched messages in in deleted vault. +- When the recovery is in progress, there will be a progess bar running at top of screen that does not block user's actions. User can continue using Tmail while the recovery process is runing. +- When the recovery is completed, the system will restore 5 (configurable by system) first messages in the result to a new system folder:"Recovered messages". There will be a toast message: "Recover deleted messages successfully". And an Open button. When I click this button, I am redirected to the folder Restored message +- In this new system folder, I can see the list of recovered deleted messages: + - With the folder, I can do actions :"Open in new tab"/"Mark as read" + - With the emails, I can do actions: Spam/Unspam, Star/Unstar, Read/Unread, Move, Delete, Open in a new tab, Create a new rule with this email . + +[Back to Summary](#summary) + +## Screenshots + +![Group 791 (2)](https://github.com/linagora/tmail-flutter/assets/68209176/20d9de2c-e183-4fe4-89b6-1adacedd2cc4) + +![Group 792 (3)](https://github.com/linagora/tmail-flutter/assets/68209176/d8d3cd22-5392-49fa-8b8c-249d782f2d6c) + +![Group 806](https://github.com/linagora/tmail-flutter/assets/68209176/f5b0b8fd-db61-44c8-a523-aa96200417f1) + +![Group 807](https://github.com/linagora/tmail-flutter/assets/68209176/d7a65e38-e399-4719-a97d-6eee4a972ad3) + + +[Back to Summary](#summary) + +## Misc + +None + +[Back to Summary](#summary) diff --git a/docs/stories/TF-1489-TeamMail-Calender-Events.md b/docs/stories/TF-1489-TeamMail-Calender-Events.md new file mode 100644 index 0000000000..18365fd842 --- /dev/null +++ b/docs/stories/TF-1489-TeamMail-Calender-Events.md @@ -0,0 +1,80 @@ +# Summary + +* [Related EPIC](#related-epic) +* [Definition](#definition) +* [Screenshots](#screenshots) +* [Misc](#misc) + +## Related EPIC + +* TODO: {link} + +## Definition + +- Given that I am a Tmail user, I have logged-in successfully and I can view calendar events. + +**UC1. As an user, I want to see the calendar events invitation via email on thread-view** + _On thread-view, I can see email of calendar events which having title include:_ +- The icon indicator events in front of email title +- The icon indicator .ics file which having in events email +- The event name on subject +- The event short description +- The host name (sender) email who be created an event +- The event status (New / Updated / Canceled) +- **Expected** event email: **[Events icon] EventStatus from HostName (email description) [Attachment icon]** + + **_New event sample:_** **[Events icon] New event from Benoît TELLIER** _Benoît TELLIER has invited you to a meeting EventName Time: Friday 10 March 2023 11:00 - 12:00..._ [Attachment icon] + **_Updated event sample:_** **[Events icon] EventName from Benoît TELLIER updated** _Benoît TELLIER has updated a meeting EventName TimeChanged: Friday 10 March 2023 11:00 - 12:00..._ [Attachment icon] + **_Canceled event sample:_** **[Events icon] EventName from Benoît TELLIER canceled** _Benoît TELLIER has canceled a meeting EventName Time: Friday 10 March 2023 11:00 - 12:00..._ [Attachment icon] + +**UC2. As an user, I want to see the calendar events invitation via email on details-view** + _On details-view, I can see description of calendar events which having description include:_ +- The Calendar icon indicator event date +- The event name on subject: + + **_New event sample:_** **New event from Benoît TELLIER: TFK + **_Updated event sample:_** **Event TFK from Benoît TELLIER updated + **_Canceled event sample:_** **Event TFK from Benoît TELLIER canceled + +- The notification with color to describe each event status: + + **_New event sample: GREEN COLOR_** **Benoît TELLIER has invited you to a meeting + **_Updated event sample: YEALLOW COLOR_** **Benoît TELLIER has updated a meeting + **_Canceled event sample: RED COLOR_** **Benoît TELLIER has canceled a meeting + +- The mini tips information of event (When event will be started!? Who are invited !? Where is the event place!?) +- The description will be demonstrated in email description + + + +**NOTE**: We will implement **Event calendar - User decision ** in another story **Yes/Maybe/No** button to select my decision _(Only display in case event status = New/Update, not display in case event status = Canceled)_ + + + + +[Back to Summary](#summary) + +## Screenshots: + +![Image](https://user-images.githubusercontent.com/124866146/229365003-93d58c56-84b9-464f-8c3c-0ba8094bc8c9.png) + + +![Image](https://user-images.githubusercontent.com/124866146/229364878-cd75529e-949d-4f69-8f22-93721b94874f.png) + + +![Image](https://user-images.githubusercontent.com/124866146/229364925-e6729d3e-a06f-4cc4-9ec8-ea63d6337a2b.png) + + +![Image](https://user-images.githubusercontent.com/124866146/229364955-c819a54c-d420-4dfb-ad89-8bc859e7d9e9.png) + + + +None + +[Back to Summary](#summary) + +## Misc + +None + +[Back to Summary](#summary) diff --git a/docs/stories/TF-1643-user-can-create-filter-rule-with-label.md b/docs/stories/TF-1643-user-can-create-filter-rule-with-label.md new file mode 100644 index 0000000000..80d65e55e2 --- /dev/null +++ b/docs/stories/TF-1643-user-can-create-filter-rule-with-label.md @@ -0,0 +1,40 @@ +# Summary + +* [Related EPIC](#related-epic) +* [Definition](#definition) +* [Screenshots](#screenshots) +* [Misc](#misc) + +## Related EPIC + +https://github.com/linagora/tmail-flutter/issues/1638 + +## Definition + +- Given that I am a Tmail user and I have logged in successfully +- On the section: Perform the following action, When I click on action list, there is a new option :Label as +- I select action "Label as" then click on the next field Select label +- There will be a list of available labels +- I can select one or multiple labels +- The selected lable will be displayed as tags on the field with a cross icon that I can click on to remove +- After inputting other fields and click button Create, the new rule is created +- When a new message matches with the condition, it will be labeled automatically as in the rule. + +[Back to Summary](#summary) + +## Screenshots + +![image](https://user-images.githubusercontent.com/68209176/228729483-28d593d9-5ef9-4eea-87f5-b3b3934576f3.png) + +![image](https://user-images.githubusercontent.com/68209176/228729525-25065c92-1227-45a4-80cb-9ba0bcb51e38.png) + +![image](https://user-images.githubusercontent.com/68209176/228729560-34885bcd-8b55-450e-b78a-803189fcf808.png) + + +[Back to Summary](#summary) + +## Misc + +None + +[Back to Summary](#summary) diff --git a/docs/stories/TF-1786-TeamMail-Swipe-in-Email-item-in-ThreadView .md b/docs/stories/TF-1786-TeamMail-Swipe-in-Email-item-in-ThreadView .md new file mode 100644 index 0000000000..aeff98df06 --- /dev/null +++ b/docs/stories/TF-1786-TeamMail-Swipe-in-Email-item-in-ThreadView .md @@ -0,0 +1,88 @@ +# Summary + +* [Related EPIC](#related-epic) +* [Definition](#definition) +* [Screenshots](#screenshots) +* [Misc](#misc) + +## Related EPIC + +* TODO: {link} + +## Definition: +**UC1. As I am a user, I want to swipe left to right to mark an email as read without opening this email +On thread-view, when I swipe left to right on email which be unread state, I can see: + +- The icon indicator this is unread email +- The background color is blue + +**Expected:** User mark an email as read successfully. + +``` +GIVEN I'm a Tmail user +AND There are existed unread emails on my mailbox +WHEN I swipe left to right on an unread email rows +THEN I see the email row is swiped as blue background color belong with "read email icon" +AND System mark an email as read +``` +![image](https://user-images.githubusercontent.com/124866146/235097387-fb616f8f-ff50-4dfe-8e2b-44a5bca3aba4.png) + + +**UC2. As I am a user, I want to swipe left to right to mark an email as unread without opening this email +On thread-view, when I swipe left to right on email which be unread state, I can see: + +- The icon indicator this is read email +- The background color is blue + +**Expected:** User mark an email as unread successfully. + +``` +GIVEN I'm a Tmail user +AND There are existed read emails on my mailbox +WHEN I swipe left to right on an read email rows +THEN I see the email row is swiped as blue background color belong with "unread email icon" +AND System mark an email as unread +``` +![image](https://user-images.githubusercontent.com/124866146/235097306-615f0b87-5fe2-456b-b830-072ddc348059.png) + +**UC3. As I am a user, I want to swipe right to left to move an email +On thread-view, when I swipe right to left on email in my mailbox, I can see: + +- The icon indicator this is Move email +- The background color is blue +- The list of mailboxes will be displayed +- The checked icon will be displayed which indicator that email is in what mailbox at the moment + +**Expected:** +I see the email row is swiped as blue background color belong with "Move email icon" +AND System display the list of mailboxes wit checked icon belong to mailbox which include that email. +AND This email is moved to selected mailbox. System display a toast message. + +``` +GIVEN I'm a Tmail user +AND There are existed emails on my mailboxes +WHEN I swipe right to left on an email rows +THEN I see the email row is swiped as blue background color belong with "Move email icon" +AND System display a list mailbox dialog to let user select the email moving desitination +AND System display a checked icon belong to mailbox in the list which indicate user that email is in what mailbox at the moment + +GIVEN I'm a Tmail user +WHEN I swipe right to left on an email rows +AND I select one mailbox in the list +THEN The email is moved with the toast message +``` + +![image](https://user-images.githubusercontent.com/124866146/236110914-0b53392a-161a-4f25-85d9-abb06706d82a.png) + +![image](https://user-images.githubusercontent.com/124866146/236111202-a4154949-5512-4287-bc78-d85ad6ad610a.png) + +![image](https://user-images.githubusercontent.com/124866146/236111590-6d85c76f-e9ab-4128-ae11-060c3186085f.png) + + +[Back to Summary](#summary) + +## Misc + +None + +[Back to Summary](#summary) diff --git a/docs/users/user-guide.md b/docs/users/user-guide.md new file mode 100644 index 0000000000..70c4d91d5f --- /dev/null +++ b/docs/users/user-guide.md @@ -0,0 +1,100 @@ +# TMail User Documentation + +Welcome to TMail! This user documentation is designed to provide you with a detailed tour of the application, including productivity tips and step-by-step instructions on how to make the most of TMail's capabilities. Whether you're new to the platform or looking to enhance your TMail experience, you'll find valuable information here. + +## Table of Contents + +1. [Authentication](#1-authentication) +2. [Reading Emails and Email Actions](#2-reading-emails-and-email-actions) +3. [Writing an Email (+Drafts)](#3-writing-an-email-drafts) +4. [Managing Folders](#4-managing-folders) +5. [Search](#5-search) +6. [Account Management](#6-account-management) +7. [Writing an Email (+Drafts) (Mobile Part)](#7-writing-an-email-drafts-mobile-part) + +## 1. Authentication + +To get started with TMail, you'll need to sign in to your account: + +1. Open the TMail application. +2. Click on the "Sign In" button. +3. Enter your email address and password. +4. Click "Sign In" again. + +## 2. Reading Emails and Email Actions + +### 2.1. Reading Emails + +Once you're logged in, you'll see your inbox. Here's how to read emails: + +- Click on an email in the inbox to open it. +- You can now read the email's contents. + +### 2.2. Email Actions + +TMail provides various actions you can take with emails: + +- **Reply:** Click the "Reply" button to respond to the sender. +- **Forward:** Click the "Forward" button to send the email to someone else. +- **Delete:** Use the "Delete" button to remove emails from your inbox. +- **Mark as Read/Unread:** Toggle the email's status using the "Mark as Read" or "Mark as Unread" option. +- **Archive:** Click the "Archive" button to move the email to the archive folder. + +## 3. Writing an Email (+Drafts) + +### 3.1. Composing a New Email + +To compose a new email: + +1. Click on the "Compose" button. +2. Fill in the recipient's email address, subject, and message. +3. Click "Send" when you're ready to send the email. + +### 3.2. Drafts + +TMail allows you to save email drafts for later: + +1. While composing an email, click the "Save Draft" button. +2. Your draft will be saved and can be accessed from the drafts folder. + +## 4. Managing Folders + +TMail lets you organize your emails efficiently: + +- **Create Folders:** Click the "New Folder" option to create custom folders for organizing your emails. +- **Move Emails:** Select one or more emails and use the "Move to" option to place them in specific folders. +- **Delete Folders:** You can delete folders by right-clicking on them and selecting "Delete Folder." + +## 5. Search + +Efficiently find emails using TMail's search feature: + +1. Enter your search query in the search bar. +2. TMail will display matching emails in your inbox. + +## 6. Account Management + +Manage your TMail account settings: + +- Click on your profile picture or name in the top right corner. +- Access settings to update your profile information, change your password, and configure preferences. + +## 7. Writing an Email (+Drafts) (Mobile Part) + +### 7.1. Composing Emails on Mobile + +Using TMail on your mobile device is just as easy: + +1. Open the TMail mobile app. +2. Tap the "Compose" icon. +3. Fill in the recipient's email address, subject, and message. +4. Tap "Send" to send the email. + +### 7.2. Drafts on Mobile + +Accessing drafts on mobile: + +1. Tap the menu icon (usually three horizontal lines). +2. Select "Drafts" from the menu to view and edit your saved drafts. + +Congratulations! You've now learned how to use TMail effectively, from authentication to managing your emails on both desktop and mobile devices. If you have any further questions or need assistance, please refer to the TMail support resources or contact our customer support team. Enjoy your efficient and organized email experience with TMail! diff --git a/env.file b/env.file index 6c44a3be4e..273e74325b 100644 --- a/env.file +++ b/env.file @@ -1,6 +1,7 @@ SERVER_URL=http://localhost/ DOMAIN_REDIRECT_URL=http://localhost:3000 WEB_OIDC_CLIENT_ID=teammail-web +OIDC_SCOPES=openid,profile,email,offline_access APP_GRID_AVAILABLE=supported FCM_AVAILABLE=supported IOS_FCM=supported \ No newline at end of file diff --git a/fcm/analysis_options.yaml b/fcm/analysis_options.yaml index a5744c1cfb..0f32754d37 100644 --- a/fcm/analysis_options.yaml +++ b/fcm/analysis_options.yaml @@ -1,4 +1,16 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +linter: + rules: + constant_identifier_names: false + non_constant_identifier_names: false + unnecessary_string_escapes: false \ No newline at end of file diff --git a/fcm/pubspec.lock b/fcm/pubspec.lock index a5bc31b739..dffc02fb4b 100644 --- a/fcm/pubspec.lock +++ b/fcm/pubspec.lock @@ -5,198 +5,218 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "61.0.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "5.13.0" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.2.0" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" source: hosted version: "7.2.7" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" + url: "https://pub.dev" source: hosted - version: "8.4.2" + version: "8.4.4" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" + version: "1.3.0" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.5.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.1" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352" + url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" dartz: dependency: transitive description: name: dartz - url: "https://pub.dartlang.org" + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" source: hosted version: "0.10.1" dio: - dependency: transitive + dependency: "direct main" description: name: dio - url: "https://pub.dartlang.org" + sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.0.0" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted version: "6.1.4" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -206,9 +226,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_test: dependency: "direct dev" description: flutter @@ -218,57 +239,64 @@ packages: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.2.0" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" source: hosted version: "2.1.1" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" source: hosted version: "2.2.0" http_mock_adapter: dependency: "direct main" description: name: http_mock_adapter - url: "https://pub.dartlang.org" + sha256: "0e7eaa5d77a273af1c2b5ec5066578faaa73039b63ccda5263c200756f24441a" + url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.4.2" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" jmap_dart_client: dependency: "direct main" description: path: "." ref: master - resolved-ref: "66ef334ac2cf4d5cb30835b30094a51802fae2ba" + resolved-ref: e8005e28b48ee06259d4f51045a58f20c891e0b9 url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" @@ -276,126 +304,144 @@ packages: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: "direct main" description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.8.0" json_serializable: dependency: "direct dev" description: name: json_serializable - url: "https://pub.dartlang.org" + sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a + url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.6.1" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.15" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.9.1" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.4" mockito: dependency: "direct dev" description: name: mockito - url: "https://pub.dartlang.org" + sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.4.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.8.3" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted version: "1.5.1" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" source: hosted - version: "3.0.1+1" + version: "3.2.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted version: "1.0.3" sky_engine: @@ -407,107 +453,122 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298 + url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.2.7" source_helper: dependency: transitive description: name: source_helper - url: "https://pub.dartlang.org" + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" source: hosted version: "1.3.3" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.5.1" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=2.5.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.0.0" diff --git a/fcm/pubspec.yaml b/fcm/pubspec.yaml index ab0ec80648..ae1e8acd9d 100644 --- a/fcm/pubspec.yaml +++ b/fcm/pubspec.yaml @@ -5,34 +5,39 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter - equatable: 2.0.3 - - json_annotation: 4.5.0 - + ### Dependencies from git ### jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git ref: master - http_mock_adapter: 0.3.2 + ### Dependencies from pub.dev ### + equatable: 2.0.5 + + json_annotation: 4.8.0 + + dio: 5.0.0 + + http_mock_adapter: 0.4.2 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: 1.0.4 + flutter_lints: 2.0.1 + + build_runner: 2.3.3 - build_runner: 2.1.11 + json_serializable: 6.6.1 - json_serializable: 6.2.0 + mockito: 5.4.2 - mockito: 5.2.0 flutter: uses-material-design: true \ No newline at end of file diff --git a/fcm/test/method/firebase_subscription_get_method_test.dart b/fcm/test/method/firebase_subscription_get_method_test.dart index df122b4d87..c91da43b73 100644 --- a/fcm/test/method/firebase_subscription_get_method_test.dart +++ b/fcm/test/method/firebase_subscription_get_method_test.dart @@ -76,8 +76,7 @@ void main() { }, headers: { "accept": "application/json;jmapVersion=rfc-8621", - "content-type": "application/json; charset=utf-8", - "content-length": 133 + "content-length": 162 } ); diff --git a/fcm/test/method/firebase_subscription_set_method_test.dart b/fcm/test/method/firebase_subscription_set_method_test.dart index bb50212b5d..86379c17f6 100644 --- a/fcm/test/method/firebase_subscription_set_method_test.dart +++ b/fcm/test/method/firebase_subscription_set_method_test.dart @@ -69,8 +69,7 @@ void main() { }, headers: { "accept": "application/json;jmapVersion=rfc-8621", - "content-type": "application/json; charset=utf-8", - "content-length": 253 + "content-length": 424 } ); diff --git a/forward/analysis_options.yaml b/forward/analysis_options.yaml index a5744c1cfb..0f32754d37 100644 --- a/forward/analysis_options.yaml +++ b/forward/analysis_options.yaml @@ -1,4 +1,16 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +linter: + rules: + constant_identifier_names: false + non_constant_identifier_names: false + unnecessary_string_escapes: false \ No newline at end of file diff --git a/forward/lib/forward/converter/forward_id_coverter.dart b/forward/lib/forward/converter/forward_id_coverter.dart deleted file mode 100644 index a0c8b4a683..0000000000 --- a/forward/lib/forward/converter/forward_id_coverter.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:forward/forward/forward_id.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; -import 'package:json_annotation/json_annotation.dart'; - -class ForwardIdConverter implements JsonConverter { - const ForwardIdConverter(); - - @override - ForwardId fromJson(String json) => ForwardId(id: Id(json)); - - @override - String toJson(ForwardId object) => object.id.value; -} diff --git a/forward/lib/forward/get/get_forward_method.dart b/forward/lib/forward/get/get_forward_method.dart index 81976cba58..9ed2b1ab10 100644 --- a/forward/lib/forward/get/get_forward_method.dart +++ b/forward/lib/forward/get/get_forward_method.dart @@ -1,5 +1,4 @@ import 'package:forward/forward/capability_forward.dart'; -import 'package:forward/forward/converter/forward_id_coverter.dart'; import 'package:jmap_dart_client/http/converter/account_id_converter.dart'; import 'package:jmap_dart_client/http/converter/id_converter.dart'; import 'package:jmap_dart_client/http/converter/properties_converter.dart'; @@ -13,7 +12,6 @@ import 'package:json_annotation/json_annotation.dart'; part 'get_forward_method.g.dart'; @IdConverter() -@ForwardIdConverter() @AccountIdConverter() @PropertiesConverter() @JsonSerializable(explicitToJson: true) diff --git a/forward/lib/forward/tmail_forward.dart b/forward/lib/forward/tmail_forward.dart index 28011d8816..6952aaa874 100644 --- a/forward/lib/forward/tmail_forward.dart +++ b/forward/lib/forward/tmail_forward.dart @@ -1,4 +1,3 @@ -import 'package:forward/forward/converter/forward_id_coverter.dart'; import 'package:forward/forward/converter/forward_id_nullable_converter.dart'; import 'package:forward/forward/forward.dart'; import 'package:forward/forward/forward_id.dart'; @@ -6,7 +5,6 @@ import 'package:json_annotation/json_annotation.dart'; part 'tmail_forward.g.dart'; -@ForwardIdConverter() @ForwardIdNullableConverter() @JsonSerializable(explicitToJson: true, includeIfNull: false) class TMailForward extends Forward { diff --git a/forward/pubspec.lock b/forward/pubspec.lock index f89b485ab6..dffc02fb4b 100644 --- a/forward/pubspec.lock +++ b/forward/pubspec.lock @@ -5,198 +5,218 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "61.0.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "5.13.0" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.2.0" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" source: hosted version: "7.2.7" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" + url: "https://pub.dev" source: hosted - version: "8.4.2" + version: "8.4.4" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.0" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.5.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.1" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352" + url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" dartz: dependency: transitive description: name: dartz - url: "https://pub.dartlang.org" + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" source: hosted version: "0.10.1" dio: - dependency: transitive + dependency: "direct main" description: name: dio - url: "https://pub.dartlang.org" + sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.0.0" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted version: "6.1.4" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -206,9 +226,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_test: dependency: "direct dev" description: flutter @@ -218,57 +239,64 @@ packages: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.2.0" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" source: hosted version: "2.2.0" http_mock_adapter: dependency: "direct main" description: name: http_mock_adapter - url: "https://pub.dartlang.org" + sha256: "0e7eaa5d77a273af1c2b5ec5066578faaa73039b63ccda5263c200756f24441a" + url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.4.2" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" jmap_dart_client: dependency: "direct main" description: path: "." ref: master - resolved-ref: "45ea109f70be0d868b005aadf11a39e2ac816c38" + resolved-ref: e8005e28b48ee06259d4f51045a58f20c891e0b9 url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" @@ -276,128 +304,146 @@ packages: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: "direct main" description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.8.0" json_serializable: dependency: "direct dev" description: name: json_serializable - url: "https://pub.dartlang.org" + sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a + url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.6.1" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.15" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.9.1" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.4" mockito: dependency: "direct dev" description: name: mockito - url: "https://pub.dartlang.org" + sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.4.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.8.3" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted version: "1.5.1" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" source: hosted - version: "3.0.1+1" + version: "3.2.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" sky_engine: dependency: transitive description: flutter @@ -407,107 +453,122 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298 + url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.2.7" source_helper: dependency: transitive description: name: source_helper - url: "https://pub.dartlang.org" + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" source: hosted version: "1.3.3" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.5.1" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=2.5.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.0.0" diff --git a/forward/pubspec.yaml b/forward/pubspec.yaml index 179e125831..94181ea0ca 100644 --- a/forward/pubspec.yaml +++ b/forward/pubspec.yaml @@ -5,34 +5,39 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter - equatable: 2.0.3 - - json_annotation: 4.5.0 - + ### Dependencies from git ### jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git ref: master - http_mock_adapter: 0.3.2 + ### Dependencies from pub.dev ### + equatable: 2.0.5 + + json_annotation: 4.8.0 + + dio: 5.0.0 + + http_mock_adapter: 0.4.2 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: 1.0.4 + flutter_lints: 2.0.1 + + build_runner: 2.3.3 - build_runner: 2.1.11 + json_serializable: 6.6.1 - json_serializable: 6.2.0 + mockito: 5.4.2 - mockito: 5.2.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/forward/test/forward/get_forward_method_test.dart b/forward/test/forward/get_forward_method_test.dart index 53956aa1e1..64fcee0a1d 100644 --- a/forward/test/forward/get_forward_method_test.dart +++ b/forward/test/forward/get_forward_method_test.dart @@ -64,8 +64,7 @@ void main() { }, headers: { "accept": "application/json;jmapVersion=rfc-8621", - "content-type": "application/json; charset=utf-8", - "content-length": 212 + "content-length": 292 }); final httpClient = HttpClient(dio); diff --git a/forward/test/forward/set_forward_method_test.dart b/forward/test/forward/set_forward_method_test.dart index c7cff75910..9a3010a737 100644 --- a/forward/test/forward/set_forward_method_test.dart +++ b/forward/test/forward/set_forward_method_test.dart @@ -98,8 +98,7 @@ void main() { }, headers: { "accept": "application/json;jmapVersion=rfc-8621", - "content-type": "application/json; charset=utf-8", - "content-length": 420 + "content-length": 765 } ); diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index f828bd2200..b5936e87e9 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 9.0 + 11.0 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 25b31c90db..7a6e8a0a66 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,9 +1,10 @@ PODS: - - AppAuth (1.4.0): - - AppAuth/Core (= 1.4.0) - - AppAuth/ExternalUserAgent (= 1.4.0) - - AppAuth/Core (1.4.0) - - AppAuth/ExternalUserAgent (1.4.0) + - AppAuth (1.6.0): + - AppAuth/Core (= 1.6.0) + - AppAuth/ExternalUserAgent (= 1.6.0) + - AppAuth/Core (1.6.0) + - AppAuth/ExternalUserAgent (1.6.0): + - AppAuth/Core - better_open_file (0.0.1): - Flutter - connectivity_plus (0.0.1): @@ -44,6 +45,8 @@ PODS: - DKPhotoGallery/Resource (0.0.17): - SDWebImage - SwiftyGif + - external_app_launcher (0.0.1): + - Flutter - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter @@ -52,10 +55,10 @@ PODS: - Firebase/Messaging (10.3.0): - Firebase/CoreOnly - FirebaseMessaging (~> 10.3.0) - - firebase_core (2.4.0): + - firebase_core (2.7.0): - Firebase/CoreOnly (= 10.3.0) - Flutter - - firebase_messaging (14.1.4): + - firebase_messaging (14.2.5): - Firebase/Messaging (= 10.3.0) - firebase_core - Flutter @@ -63,9 +66,9 @@ PODS: - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreInternal (10.3.0): + - FirebaseCoreInternal (10.5.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseInstallations (10.3.0): + - FirebaseInstallations (10.5.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) @@ -82,12 +85,14 @@ PODS: - fk_user_agent (2.0.0): - Flutter - Flutter (1.0.0) + - flutter_app_badger (1.3.0): + - Flutter - flutter_appauth (0.0.1): - - AppAuth (= 1.4.0) + - AppAuth (= 1.6.0) - Flutter - flutter_downloader (0.0.1): - Flutter - - flutter_image_compress (0.0.1): + - flutter_image_compress (1.0.0): - Flutter - Mantle - SDWebImage @@ -105,32 +110,26 @@ PODS: - Flutter - flutter_native_splash (0.0.1): - Flutter - - fluttertoast (0.0.2): - - Flutter - - Toast - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - - GoogleDataTransport (9.2.0): + - GoogleDataTransport (9.2.1): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.10.0): + - GoogleUtilities/AppDelegateSwizzler (7.11.0): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.10.0): + - GoogleUtilities/Environment (7.11.0): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.10.0): + - GoogleUtilities/Logger (7.11.0): - GoogleUtilities/Environment - - GoogleUtilities/Network (7.10.0): + - GoogleUtilities/Network (7.11.0): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.10.0)" - - GoogleUtilities/Reachability (7.10.0): + - "GoogleUtilities/NSData+zlib (7.11.0)" + - GoogleUtilities/Reachability (7.11.0): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.10.0): + - GoogleUtilities/UserDefaults (7.11.0): - GoogleUtilities/Logger - libwebp (1.2.4): - libwebp/demux (= 1.2.4) @@ -152,32 +151,30 @@ PODS: - OrderedSet (5.0.0) - package_info_plus (0.4.5): - Flutter - - path_provider (0.0.1): + - path_provider_foundation (0.0.1): - Flutter + - FlutterMacOS - permission_handler_apple (9.0.4): - Flutter - - PromisesObjC (2.1.1) + - PromisesObjC (2.2.0) - ReachabilitySwift (5.0.0) - receive_sharing_intent (0.0.1): - Flutter - - SDWebImage (5.14.2): - - SDWebImage/Core (= 5.14.2) - - SDWebImage/Core (5.14.2) - - SDWebImageWebPCoder (0.9.1): + - SDWebImage (5.15.4): + - SDWebImage/Core (= 5.15.4) + - SDWebImage/Core (5.15.4) + - SDWebImageWebPCoder (0.10.1): - libwebp (~> 1.0) - - SDWebImage/Core (~> 5.13) - - share (0.0.1): - - Flutter - - shared_preferences (0.0.1): + - SDWebImage/Core (~> 5.15) + - share_plus (0.0.1): - Flutter - - sqflite (0.0.2): + - shared_preferences_foundation (0.0.1): - Flutter - - FMDB (>= 2.7.5) - - SwiftyGif (5.4.3) - - Toast (4.0.0) + - FlutterMacOS + - SwiftyGif (5.4.4) - url_launcher_ios (0.0.1): - Flutter - - webview_flutter_wkwebview (0.0.1): + - workmanager (0.0.1): - Flutter DEPENDENCIES: @@ -185,11 +182,13 @@ DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - contacts_service (from `.symlinks/plugins/contacts_service/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - external_app_launcher (from `.symlinks/plugins/external_app_launcher/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - fk_user_agent (from `.symlinks/plugins/fk_user_agent/ios`) - Flutter (from `Flutter`) + - flutter_app_badger (from `.symlinks/plugins/flutter_app_badger/ios`) - flutter_appauth (from `.symlinks/plugins/flutter_appauth/ios`) - flutter_downloader (from `.symlinks/plugins/flutter_downloader/ios`) - flutter_image_compress (from `.symlinks/plugins/flutter_image_compress/ios`) @@ -197,16 +196,14 @@ DEPENDENCIES: - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider (from `.symlinks/plugins/path_provider/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - - share (from `.symlinks/plugins/share/ios`) - - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) + - workmanager (from `.symlinks/plugins/workmanager/ios`) SPEC REPOS: trunk: @@ -218,7 +215,6 @@ SPEC REPOS: - FirebaseCoreInternal - FirebaseInstallations - FirebaseMessaging - - FMDB - GoogleDataTransport - GoogleUtilities - libwebp @@ -230,7 +226,6 @@ SPEC REPOS: - SDWebImage - SDWebImageWebPCoder - SwiftyGif - - Toast EXTERNAL SOURCES: better_open_file: @@ -241,6 +236,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/contacts_service/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" + external_app_launcher: + :path: ".symlinks/plugins/external_app_launcher/ios" file_picker: :path: ".symlinks/plugins/file_picker/ios" firebase_core: @@ -251,6 +248,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fk_user_agent/ios" Flutter: :path: Flutter + flutter_app_badger: + :path: ".symlinks/plugins/flutter_app_badger/ios" flutter_appauth: :path: ".symlinks/plugins/flutter_appauth/ios" flutter_downloader: @@ -265,75 +264,69 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" - fluttertoast: - :path: ".symlinks/plugins/fluttertoast/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider: - :path: ".symlinks/plugins/path_provider/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" receive_sharing_intent: :path: ".symlinks/plugins/receive_sharing_intent/ios" - share: - :path: ".symlinks/plugins/share/ios" - shared_preferences: - :path: ".symlinks/plugins/shared_preferences/ios" - sqflite: - :path: ".symlinks/plugins/sqflite/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" - webview_flutter_wkwebview: - :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" + workmanager: + :path: ".symlinks/plugins/workmanager/ios" SPEC CHECKSUMS: - AppAuth: 31bcec809a638d7bd2f86ea8a52bd45f6e81e7c7 + AppAuth: 8fca6b5563a5baef2c04bee27538025e4ceb2add better_open_file: 03cf320415d4d3f46b6e00adc4a567d76c1a399d connectivity_plus: 413a8857dd5d9f1c399a39130850d02fe0feaf7e contacts_service: 849e1f84281804c8bfbec1b4c3eedcb23c5d3eca device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: 817ab1d8cd2da9d2da412a417162deee3500fc95 + external_app_launcher: ad55ac844aa21f2d2197d7cec58ff0d0dc40bbc0 + file_picker: ce3938a0df3cc1ef404671531facef740d03f920 Firebase: f92fc551ead69c94168d36c2b26188263860acd9 - firebase_core: 6f2f753e316765799d88568232ed59e300ff53db - firebase_messaging: 4b60bd563e1d21e3fe91afd398b59d0eda79cc60 + firebase_core: 128d8c43c3a453a4a67463314fc3761bedff860b + firebase_messaging: 8ac28baba96e69a58ad26b4bb5f9ee67c3e5efe2 FirebaseCore: 988754646ab3bd4bdcb740f1bfe26b9f6c0d5f2a - FirebaseCoreInternal: 29b76f784d607df8b2a1259d73c3f04f1210137b - FirebaseInstallations: e2f26126089dcf41e215f7b8925af8d953c7d602 + FirebaseCoreInternal: e463f41bb935cd049505bf7e9a5bdd7dcea90df6 + FirebaseInstallations: 935bc4abb6f7a035cab7a0c31cb777b2be3dd254 FirebaseMessaging: e345b219fd15d325f0cf2fef28cb8ce00d851b3f fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545 - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - flutter_appauth: 05c3778a1e4ae23364dd2ef37cbae14b244f646b - flutter_downloader: 058b9c41564a90500f67f3e432e3524613a7fd83 - flutter_image_compress: fd2b476345226e1a10ea352fa306af95704642c1 - flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 + Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + flutter_app_badger: b87fc231847b03b92ce1412aa351842e7e97932f + flutter_appauth: 525652bda90e43ca5eb7ed748ecc106b79d1f5a1 + flutter_downloader: b7301ae057deadd4b1650dc7c05375f10ff12c39 + flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433 + flutter_inappwebview: 4fe74e5e65809c3d363febfd9e2b21aa79bb0f1c flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef - fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - GoogleDataTransport: 1c8145da7117bd68bbbed00cf304edb6a24de00f - GoogleUtilities: bad72cb363809015b1f7f19beb1f1cd23c589f95 + GoogleDataTransport: ea169759df570f4e37bdee1623ec32a7e64e67c4 + GoogleUtilities: c2bdc4cf2ce786c4d2e6b3bcfd599a25ca78f06f libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c + path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce - PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb + PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1 - SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84 - SDWebImageWebPCoder: 18503de6621dd2c420d680e33d46bf8e1d5169b0 - share: 0b2c3e82132f5888bccca3351c504d0003b3b410 - shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d - sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 - SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780 - Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de - webview_flutter_wkwebview: b7e70ef1ddded7e69c796c7390ee74180182971f + SDWebImage: 1c39de67663e5eebb2f41324d5d580eeea12dd4c + SDWebImageWebPCoder: 4851414d9f8894e328e8b97c93ea4f4f4e4418ae + share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 + shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472 + SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f + url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 PODFILE CHECKSUM: e209bf81fb4facf3f68c43dbffa0b2d05ff63db0 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 60afbed36c..ac0913bd66 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -301,10 +301,12 @@ }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -337,6 +339,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -448,7 +451,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -466,6 +469,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = KUT463DS29; @@ -532,7 +536,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -581,7 +585,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -601,6 +605,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = KUT463DS29; @@ -630,9 +635,12 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; DEVELOPMENT_TEAM = KUT463DS29; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.0; @@ -642,7 +650,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.teammail; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = tmail.development.profile; + PROVISIONING_PROFILE_SPECIFIER = tmail.distribution.profile; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = tmail.distribution.profile; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; VERSIONING_SYSTEM = "apple-generic"; @@ -659,9 +668,11 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = TeamMailShareExtension/TeamMailShareExtension.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = KUT463DS29; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TeamMailShareExtension/Info.plist; @@ -678,7 +689,8 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.teammail.TeamMailShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = tmail.ext.ios.development.provisioning.profile; + PROVISIONING_PROFILE_SPECIFIER = tmail.share.ext.development.profile; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = tmail.share.ext.development.profile; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_EMIT_LOC_STRINGS = YES; @@ -698,9 +710,12 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = TeamMailShareExtension/TeamMailShareExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Distribution"; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = KUT463DS29; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TeamMailShareExtension/Info.plist; @@ -716,7 +731,8 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.teammail.TeamMailShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = tmail.ext.ios.development.provisioning.profile; + PROVISIONING_PROFILE_SPECIFIER = tmail.share.ext.distribution.profile; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = tmail.share.ext.distribution.profile; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -734,6 +750,7 @@ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_ENTITLEMENTS = TeamMailShareExtension/TeamMailShareExtension.entitlements; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = KUT463DS29; @@ -752,7 +769,7 @@ MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.teammail.TeamMailShareExtension; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = tmail.ext.ios.development.provisioning.profile; + PROVISIONING_PROFILE_SPECIFIER = tmail.share.ext.development.profile; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index b297e13eb5..f293d82caf 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -3,23 +3,34 @@ import Flutter import flutter_downloader import receive_sharing_intent import flutter_local_notifications +import workmanager @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { + + /// Registers all pubspec-referenced Flutter plugins in the given registry. + static func registerPlugins(with registry: FlutterPluginRegistry) { + GeneratedPluginRegistrant.register(with: registry) + } + override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { + /// Register the app's plugins in the context of a normal run + AppDelegate.registerPlugins(with: self) + + UNUserNotificationCenter.current().delegate = self - FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in - GeneratedPluginRegistrant.register(with: registry) - } - if #available(iOS 10.0, *) { UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate } - GeneratedPluginRegistrant.register(with: self) + UIApplication.shared.setMinimumBackgroundFetchInterval(TimeInterval(60*15)) + + FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { registry in + AppDelegate.registerPlugins(with: registry) + } FlutterDownloaderPlugin.setPluginRegistrantCallback { registry in if (!registry.hasPlugin("FlutterDownloaderPlugin")) { @@ -32,12 +43,18 @@ import flutter_local_notifications if url.scheme == "mailto" { if let url = handleEmailAndress(open: url) { let corrected = launchOptions!.map { (key, value) in key != .url ? (key, value) : (key, url) } - + return sharingIntent.application(application, didFinishLaunchingWithOptions: Dictionary(uniqueKeysWithValues: corrected)) } } } + WorkmanagerPlugin.setPluginRegistrantCallback { registry in + AppDelegate.registerPlugins(with: registry) + } + + WorkmanagerPlugin.registerTask(withIdentifier: "com.linagora.ios.teammail.sendingQueue") + return super.application(application, didFinishLaunchingWithOptions: launchOptions) } @@ -76,4 +93,9 @@ import flutter_local_notifications return URL(string: "ShareMedia-\(appDomain)://dataUrl=\(sharedKey)#text") } + override func userNotificationCenter(_ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler(.alert) // shows banner even if app is in foreground + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e2485a43d2..18dcc92242 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -47,7 +47,8 @@ http mailto sms - tel + tel + linshare.mobile LSRequiresIPhoneOS @@ -60,8 +61,13 @@ UIBackgroundModes fetch - remote-notification + remote-notification + processing + BGTaskSchedulerPermittedIdentifiers + + com.linagora.ios.teammail.sendingQueue + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -83,5 +89,11 @@ com.apple.developer.mail-client + UIApplicationSupportsIndirectInputEvents + + UIUserInterfaceStyle + Light + UIViewControllerBasedStatusBarAppearance + diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 77cce508da..c3e65e5fd8 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -11,53 +11,24 @@ # # Uncomment the line if you want fastlane to automatically update itself -# update_fastlane +update_fastlane opt_out_usage default_platform(:ios) setup_ci if ENV['CI'] -# Import signing certificate -import_certificate( - certificate_path: "cert.p12", - certificate_password: ENV["CERTIFICATE_PASSWORD"], - keychain_name: ENV["MATCH_KEYCHAIN_NAME"] -) - -# 2 provisioning profiles, 1 for the main app and 1 for the share extension -install_provisioning_profile(path: "buildpp.mobileprovision") -install_provisioning_profile(path: "shareextpp.mobileprovision") - - platform :ios do desc "Build development version" lane :dev do - update_code_signing_settings( - use_automatic_signing: false, - path: "Runner.xcodeproj", - code_sign_identity: "Apple Development" - ) - # Update the provisioning profile for both the main app and extension - update_project_provisioning( - xcodeproj: "Runner.xcodeproj", - profile: "./buildpp.mobileprovision", - target_filter: ".*Runner.*" - ) - update_project_provisioning( - xcodeproj: "Runner.xcodeproj", - profile: "./shareextpp.mobileprovision", - target_filter: ".*TeamMailShareExtension.*" + sync_code_signing( + type: "development", + git_private_key: ENV["APPLE_CERTIFICATES_SSH_KEY"] ) build_app( scheme: "Runner", + configuration: "Debug", workspace: "Runner.xcworkspace", export_method: "development", - export_options: { - provisioningProfiles: { - "com.linagora.ios.teammail": "tmail.development.profile", - "com.linagora.ios.teammail.TeamMailShareExtension": "tmail.share.ext.development.profile" - } - } ) end @@ -67,7 +38,11 @@ platform :ios do app_store_connect_api_key( key_id: ENV["APPLE_KEY_ID"], issuer_id: ENV["APPLE_ISSUER_ID"], - key_filepath: "./apiKey.p8" + key_content: ENV["APPLE_KEY_CONTENT"] + ) + sync_code_signing( + type: "appstore", + git_private_key: ENV["APPLE_CERTIFICATES_SSH_KEY"] ) increment_build_number( build_number: latest_testflight_build_number + 1 @@ -75,32 +50,11 @@ platform :ios do increment_version_number( version_number: last_git_tag.gsub("v", "") ) - update_code_signing_settings( - use_automatic_signing: false, - path: "Runner.xcodeproj", - code_sign_identity: "Apple Distribution" - ) - # Update the provisioning profile for both the main app and extension - update_project_provisioning( - xcodeproj: "Runner.xcodeproj", - profile: "./buildpp.mobileprovision", - target_filter: ".*Runner.*" - ) - update_project_provisioning( - xcodeproj: "Runner.xcodeproj", - profile: "./shareextpp.mobileprovision", - target_filter: ".*TeamMailShareExtension.*" - ) build_app( scheme: "Runner", + configuration: "Release", workspace: "Runner.xcworkspace", - export_method: "app-store", - export_options: { - provisioningProfiles: { - "com.linagora.ios.teammail": "tmail.distribution.profile", - "com.linagora.ios.teammail.TeamMailShareExtension": "tmail.share.ext.distribution.profile" - } - } + export_method: "app-store" ) upload_to_testflight( skip_waiting_for_build_processing: true, diff --git a/ios/fastlane/Matchfile b/ios/fastlane/Matchfile new file mode 100644 index 0000000000..4bc5c1ae99 --- /dev/null +++ b/ios/fastlane/Matchfile @@ -0,0 +1,13 @@ +git_url("git@github.com:linagora/apple-certificates.git") + +storage_mode("git") + +type("development") # The default type, can be: appstore, adhoc, enterprise or development + +app_identifier(["com.linagora.ios.teammail", "com.linagora.ios.teammail.TeamMailShareExtension"]) +# username("user@fastlane.tools") # Your Apple Developer Portal username + +# For all available options run `fastlane match --help` +# Remove the # in the beginning of the line to enable the other options + +# The docs are available on https://docs.fastlane.tools/actions/match diff --git a/ios/fastlane/README.md b/ios/fastlane/README.md new file mode 100644 index 0000000000..df13b2b762 --- /dev/null +++ b/ios/fastlane/README.md @@ -0,0 +1,40 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios dev + +```sh +[bundle exec] fastlane ios dev +``` + +Build development version + +### ios release + +```sh +[bundle exec] fastlane ios release +``` + +Build and deploy release version + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/lib/features/base/base_bindings.dart b/lib/features/base/base_bindings.dart index 164cdc674b..088a07c511 100644 --- a/lib/features/base/base_bindings.dart +++ b/lib/features/base/base_bindings.dart @@ -1,4 +1,3 @@ - import 'package:get/get_instance/src/bindings_interface.dart'; abstract class BaseBindings extends Bindings { diff --git a/lib/features/base/base_controller.dart b/lib/features/base/base_controller.dart index 44eca34d61..46b5d1cbc9 100644 --- a/lib/features/base/base_controller.dart +++ b/lib/features/base/base_controller.dart @@ -4,60 +4,80 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; -import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/toast/tmail_toast.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; import 'package:core/utils/fps_manager.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:fcm/model/firebase_capability.dart'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:forward/forward/capability_forward.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:model/account/authentication_type.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.dart'; import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart'; import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart'; -import 'package:tmail_ui_user/features/base/mixin/view_as_dialog_action_mixin.dart'; -import 'package:tmail_ui_user/features/email/presentation/mdn_interactor_bindings.dart'; +import 'package:tmail_ui_user/features/caching/caching_manager.dart'; +import 'package:tmail_ui_user/features/email/presentation/bindings/mdn_interactor_bindings.dart'; +import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; +import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; +import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/contact_autocomplete_bindings.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/tmail_autocomplete_bindings.dart'; +import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/log_out_oidc_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/email_rules/bindings/email_rules_interactor_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/forward/bindings/forwarding_interactors_bindings.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/mailbox_visibility/bindings/mailbox_visibility_interactor_bindings.dart'; import 'package:tmail_ui_user/features/push_notification/domain/exceptions/fcm_exception.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/destroy_subscription_state.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/get_fcm_subscription_local.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/destroy_subscription_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_fcm_subscription_local_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/config/fcm_configuration.dart'; -import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_controller.dart'; -import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_manager.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_message_controller.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_receiver.dart'; +import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; abstract class BaseController extends GetxController with MessageDialogActionMixin, - PopupContextMenuActionMixin, - ViewAsDialogActionMixin { + PopupContextMenuActionMixin { + + final CachingManager cachingManager = Get.find(); + final languageCacheManager = Get.find(); + final authorizationInterceptors = Get.find(); + final authorizationIsolateInterceptors = Get.find(tag: BindingTag.isolateTag); + final DeleteCredentialInteractor deleteCredentialInteractor = Get.find(); + final LogoutOidcInteractor logoutOidcInteractor = Get.find(); + final DeleteAuthorityOidcInteractor deleteAuthorityOidcInteractor = Get.find(); + final _fcmReceiver = FcmReceiver.instance; + bool _isFcmEnabled = false; + + GetFCMSubscriptionLocalInteractor? _getSubscriptionLocalInteractor; + DestroySubscriptionInteractor? _destroySubscriptionInteractor; + + final AppToast _appToast = Get.find(); + final ImagePaths _imagePaths = Get.find(); final viewState = Rx>(Right(UIState.idle)); FpsCallback? fpsCallback; void consumeState(Stream> newStateStream) async { - newStateStream.listen( - (state) => onData(state), - onError: (error, stackTrace) { - logError('BaseController::consumeState():onError:error: $error'); - logError('BaseController::consumeState():onError:stackTrace: $stackTrace'); - onError(error); - }, - onDone: () => onDone() - ); + newStateStream.listen(onData, onError: onError, onDone: onDone); } void dispatchState(Either newState) { @@ -70,45 +90,116 @@ abstract class BaseController extends GetxController void onData(Either newState) { viewState.value = newState; + viewState.value.fold( + (failure) { + if (failure is FeatureFailure) { + final exception = _performFilterExceptionInError(failure.exception); + if (exception != null) { + handleExceptionAction(failure: failure, exception: exception); + } else { + handleFailureViewState(failure); + } + } else { + handleFailureViewState(failure); + } + }, + handleSuccessViewState); } - void onError(dynamic error) { - if (error is NoNetworkError) { - logError('BaseController::onError(): $error'); - return; + void onError(Object error, StackTrace stackTrace) { + logError('BaseController::onError():error: $error | stackTrace: $stackTrace'); + final exception = _performFilterExceptionInError(error); + if (exception != null) { + handleExceptionAction(exception: exception); + } else { + handleErrorViewState(error, stackTrace); } + } - final _appToast = Get.find(); - final _imagePaths = Get.find(); - final _responsiveUtils = Get.find(); + void onDone() {} - String messageError = ''; - if (error is MethodLevelErrors) { - messageError = error.message ?? error.type.value; - } else { - if (currentContext != null) { - messageError = AppLocalizations.of(currentContext!).unknownError; + Exception? _performFilterExceptionInError(dynamic error) { + logError('BaseController::_performFilterExceptionInError(): $error'); + if (error is NoNetworkError || error is ConnectionTimeout || error is InternalServerError) { + logError('BaseController::_performFilterExceptionInError(): NoNetworkError'); + if (PlatformInfo.isWeb) { + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).no_internet_connection, + actionName: AppLocalizations.of(currentContext!).skip, + onActionClick: ToastView.dismiss, + leadingSVGIcon: _imagePaths.icNotConnection, + backgroundColor: AppColor.textFieldErrorBorderColor, + textColor: Colors.white, + infinityToast: true, + ); + } + } + return error; + } else if (error is BadCredentialsException) { + logError('BaseController::_performFilterExceptionInError(): BadCredentialsException'); + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).badCredentials); } + if (authorizationInterceptors.isAppRunning) { + performInvokeLogoutAction(); + } + return error; + } else if (error is ConnectionError) { + logError('BaseController::_performFilterExceptionInError(): ConnectionError'); + if (authorizationInterceptors.isAppRunning) { + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).connectionError); + } + performInvokeLogoutAction(); + } + return error; } - if (messageError.isNotEmpty && currentContext != null && currentOverlayContext != null) { - _appToast.showBottomToast( - currentOverlayContext!, - messageError, - leadingIcon: SvgPicture.asset( - _imagePaths.icNotConnection, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!)); + return null; + } + + void handleErrorViewState(Object error, StackTrace stackTrace) {} + + void handleExceptionAction({Failure? failure, Exception? exception}) { + logError('BaseController::handleExceptionAction():failure: $failure | exception: $exception'); + } + + void handleFailureViewState(Failure failure) { + logError('BaseController::handleFailureViewState(): ${failure.runtimeType}'); + if (failure is LogoutOidcFailure) { + if (_isFcmEnabled) { + _getSubscriptionLocalAction(); + } else { + _logoutOIDCAction(); + } + } else if (failure is GetFCMSubscriptionLocalFailure) { + performInvokeLogoutAction(); + } else if (failure is DestroySubscriptionFailure) { + performInvokeLogoutAction(); } } - void onDone(); + void handleSuccessViewState(Success success) { + log('BaseController::handleSuccessViewState(): ${success.runtimeType}'); + if (success is LogoutOidcSuccess) { + if (_isFcmEnabled) { + _getSubscriptionLocalAction(); + } else { + _logoutOIDCAction(); + } + } else if (success is GetFCMSubscriptionLocalSuccess) { + final subscriptionId = success.fcmSubscription.subscriptionId; + _destroySubscriptionAction(subscriptionId); + } else if (success is DestroySubscriptionSuccess) { + performInvokeLogoutAction(); + } + } void startFpsMeter() { FpsManager().start(); @@ -164,10 +255,6 @@ abstract class BaseController extends GetxController } } - void injectMailboxVisibilityBindings() { - MailboxVisibilityInteractorBindings().dependencies(); - } - void injectFCMBindings(Session? session, AccountId? accountId) async { try { requireCapability(session!, accountId!, [FirebaseCapability.fcmIdentifier]); @@ -175,11 +262,8 @@ abstract class BaseController extends GetxController final mapEnvData = Map.from(dotenv.env); await AppUtils.loadFcmConfigFileToEnv(currentMapEnvData: mapEnvData); FcmConfiguration.initialize(); - if (!BuildUtils.isWeb) { - await LocalNotificationManager.instance.setUp(); - } FcmInteractorBindings().dependencies(); - FcmController.instance.initialize(accountId: accountId); + FcmMessageController.instance.initializeFromAccountId(accountId, session); } else { throw NotSupportFCMException(); } @@ -187,4 +271,98 @@ abstract class BaseController extends GetxController logError('BaseController::injectFCMBindings(): exception: $e'); } } -} \ No newline at end of file + + AuthenticationType get authenticationType => authorizationInterceptors.authenticationType; + + bool get isAuthenticatedWithOidc => authenticationType == AuthenticationType.oidc; + + bool _isFcmActivated(Session session, AccountId accountId) => + [FirebaseCapability.fcmIdentifier].isSupported(session, accountId) && AppConfig.fcmAvailable; + + void goToLogin({LoginArguments? arguments}) { + if (Get.currentRoute != AppRoutes.login) { + pushAndPopAll(AppRoutes.login, arguments: arguments); + } + } + + void logout(Session? session, AccountId? accountId) { + if (session == null || accountId == null) { + logError('BaseController::logout(): Session is $session OR AccountId is $accountId'); + performInvokeLogoutAction(); + return; + } + _isFcmEnabled = _isFcmActivated(session, accountId); + if (isAuthenticatedWithOidc) { + consumeState(logoutOidcInteractor.execute()); + } else { + if (_isFcmEnabled) { + _getSubscriptionLocalAction(); + } else { + _logoutAction(); + } + } + } + + void _destroySubscriptionAction(String subscriptionId) { + try { + _destroySubscriptionInteractor = Get.find(); + consumeState(_destroySubscriptionInteractor!.execute(subscriptionId)); + } catch(e) { + logError('BaseController::destroySubscriptionAction(): exception: $e'); + performInvokeLogoutAction(); + } + } + + void _getSubscriptionLocalAction() { + try { + _getSubscriptionLocalInteractor = Get.find(); + consumeState(_getSubscriptionLocalInteractor!.execute()); + } catch (e) { + logError('BaseController::getSubscriptionLocalAction(): exception: $e'); + performInvokeLogoutAction(); + } + } + + void performInvokeLogoutAction() { + log('BaseController::performInvokeLogoutAction():'); + if (isAuthenticatedWithOidc) { + _logoutOIDCAction(); + } else { + _logoutAction(); + } + } + + void _logoutAction() async { + log('BaseController::_logoutAction():'); + await Future.wait([ + deleteCredentialInteractor.execute(), + cachingManager.clearAll(), + languageCacheManager.removeLanguage(), + ]); + cachingManager.clearAllFileInStorage(); + authorizationInterceptors.clear(); + authorizationIsolateInterceptors.clear(); + if (_isFcmEnabled) { + _fcmReceiver.deleteFcmToken(); + } + await cachingManager.closeHive(); + goToLogin(arguments: LoginArguments(LoginFormType.credentialForm)); + } + + void _logoutOIDCAction() async { + log('BaseController::_logoutOIDCAction():'); + await Future.wait([ + deleteAuthorityOidcInteractor.execute(), + cachingManager.clearAll(), + languageCacheManager.removeLanguage(), + ]); + cachingManager.clearAllFileInStorage(); + authorizationIsolateInterceptors.clear(); + authorizationInterceptors.clear(); + if (_isFcmEnabled) { + _fcmReceiver.deleteFcmToken(); + } + await cachingManager.closeHive(); + goToLogin(arguments: LoginArguments(LoginFormType.ssoForm)); + } +} diff --git a/lib/features/base/base_mailbox_controller.dart b/lib/features/base/base_mailbox_controller.dart index 9af1deffd1..cfd89ba1ad 100644 --- a/lib/features/base/base_mailbox_controller.dart +++ b/lib/features/base/base_mailbox_controller.dart @@ -5,7 +5,7 @@ import 'package:core/presentation/views/bottom_popup/confirmation_dialog_action_ import 'package:core/presentation/views/dialog/confirmation_dialog_builder.dart'; import 'package:core/presentation/views/dialog/edit_text_dialog_builder.dart'; import 'package:core/presentation/views/modal_sheets/edit_text_modal_sheet_builder.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -27,6 +27,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_request.da import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/extensions/list_mailbox_node_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories_expand_mode.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; @@ -41,6 +42,7 @@ import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_na import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -67,7 +69,7 @@ abstract class BaseMailboxController extends BaseController { List allMailboxes = []; - Future buildTree( + Future buildTree( List allMailbox, {MailboxId? mailboxIdSelected} ) async { @@ -84,7 +86,7 @@ abstract class BaseMailboxController extends BaseController { allMailboxes = tupleTree.value4; } - Future refreshTree(List allMailbox) async { + Future refreshTree(List allMailbox) async { allMailboxes = allMailbox; final tupleTree = await _treeBuilder.generateMailboxTreeInUIAfterRefreshChanges( allMailbox, @@ -99,7 +101,15 @@ abstract class BaseMailboxController extends BaseController { teamMailboxesTree.value = tupleTree.value3; } - void toggleMailboxFolder(MailboxNode selectedMailboxNode) { + Future syncAllMailboxWithDisplayName(BuildContext context) async { + log("BaseMailboxController::syncAllMailboxWithDisplayName"); + final syncedMailbox = allMailboxes + .map((mailbox) => mailbox.withDisplayName(mailbox.getDisplayName(context))) + .toList(); + allMailboxes = syncedMailbox; + } + + void toggleMailboxFolder(MailboxNode selectedMailboxNode, ScrollController scrollController) { final newExpandMode = selectedMailboxNode.expandMode == ExpandMode.COLLAPSE ? ExpandMode.EXPAND : ExpandMode.COLLAPSE; @@ -112,11 +122,45 @@ abstract class BaseMailboxController extends BaseController { if (personalMailboxTree.value.updateExpandedNode(selectedMailboxNode, newExpandMode) != null) { log('toggleMailboxFolder() refresh folderMailboxTree'); personalMailboxTree.refresh(); + final childrenItems = personalMailboxTree.value.root.childrenItems ?? []; + _triggerScrollWhenExpandMailboxFolder( + childrenItems, + selectedMailboxNode, + scrollController); } if (teamMailboxesTree.value.updateExpandedNode(selectedMailboxNode, newExpandMode) != null) { log('toggleMailboxFolder() refresh teamMailboxesTree'); teamMailboxesTree.refresh(); + final childrenItems = teamMailboxesTree.value.root.childrenItems ?? []; + _triggerScrollWhenExpandMailboxFolder( + childrenItems, + selectedMailboxNode, + scrollController); + } + } + + void _triggerScrollWhenExpandMailboxFolder( + List childrenItems, + MailboxNode selectedMailboxNode, + ScrollController scrollController) async { + await Future.delayed(const Duration(milliseconds: 200)); + final lastItem = childrenItems.last; + + if (selectedMailboxNode.expandMode == ExpandMode.COLLAPSE) { + return; + } + + if (lastItem.mailboxNameAsString.contains(selectedMailboxNode.mailboxNameAsString)) { + scrollController.animateTo( + scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInToLinear); + } else { + scrollController.animateTo( + scrollController.offset + 100, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInToLinear); } } @@ -344,32 +388,17 @@ abstract class BaseMailboxController extends BaseController { mailboxIdSelected: mailboxSelected.id ); - if (BuildUtils.isWeb) { - showDialogDestinationPicker( - context: context, - arguments: arguments, - onSelectedMailbox: (destinationMailbox) { - onMovingMailboxAction( - mailboxSelected, - destinationMailbox == PresentationMailbox.unifiedMailbox - ? null - : destinationMailbox - ); - } + final destinationMailbox = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments) + : await push(AppRoutes.destinationPicker, arguments: arguments); + + if (destinationMailbox is PresentationMailbox) { + onMovingMailboxAction( + mailboxSelected, + destinationMailbox == PresentationMailbox.unifiedMailbox + ? null + : destinationMailbox ); - } else { - final destinationMailbox = await push( - AppRoutes.destinationPicker, - arguments: arguments); - - if (destinationMailbox is PresentationMailbox) { - onMovingMailboxAction( - mailboxSelected, - destinationMailbox == PresentationMailbox.unifiedMailbox - ? null - : destinationMailbox - ); - } } } } @@ -383,7 +412,7 @@ abstract class BaseMailboxController extends BaseController { }) { if (responsiveUtils.isLandscapeMobile(context) || responsiveUtils.isPortraitMobile(context)) { (ConfirmationDialogActionSheetBuilder(context) - ..messageText(AppLocalizations.of(context).message_confirmation_dialog_delete_mailbox(presentationMailbox.name?.name ?? '')) + ..messageText(AppLocalizations.of(context).message_confirmation_dialog_delete_mailbox(presentationMailbox.getDisplayName(context))) ..onCancelAction(AppLocalizations.of(context).cancel, () => popBack()) ..onConfirmAction(AppLocalizations.of(context).delete, () => onDeleteMailboxAction(presentationMailbox)) ).show(); @@ -395,7 +424,7 @@ abstract class BaseMailboxController extends BaseController { child: (ConfirmDialogBuilder(imagePaths) ..key(const Key('confirm_dialog_delete_mailbox')) ..title(AppLocalizations.of(context).delete_mailboxes) - ..content(AppLocalizations.of(context).message_confirmation_dialog_delete_mailbox(presentationMailbox.name?.name ?? '')) + ..content(AppLocalizations.of(context).message_confirmation_dialog_delete_mailbox(presentationMailbox.getDisplayName(context))) ..addIcon(SvgPicture.asset(imagePaths.icRemoveDialog, fit: BoxFit.fill)) ..colorConfirmButton(AppColor.colorConfirmActionDialog) ..styleTextConfirmButton(const TextStyle( @@ -411,22 +440,85 @@ abstract class BaseMailboxController extends BaseController { } } + List getAncestorOfMailboxNode(MailboxNode mailboxNode) { + final listAncestor = defaultMailboxTree.value.getAncestorList(mailboxNode) + ?? personalMailboxTree.value.getAncestorList(mailboxNode) + ?? teamMailboxesTree.value.getAncestorList(mailboxNode); + return listAncestor ?? []; + } + SubscribeRequest? generateSubscribeRequest( MailboxId mailboxId, MailboxSubscribeState subscribeState, MailboxSubscribeAction subscribeAction + ) { + switch(subscribeState) { + case MailboxSubscribeState.enabled: + return _generateSubscribeRequestWhenSubscribeEnabled(mailboxId, subscribeAction); + case MailboxSubscribeState.disabled: + return _generateSubscribeRequestWhenSubscribeDisabled(mailboxId, subscribeAction); + } + } + + SubscribeRequest? _generateSubscribeRequestWhenSubscribeDisabled( + MailboxId mailboxId, + MailboxSubscribeAction subscribeAction ) { final mailboxNode = findMailboxNodeById(mailboxId); - if (mailboxNode != null) { - if (mailboxNode.hasChildren()) { - final listDescendantMailboxIds = mailboxNode.descendantsAsList().mailboxIds; - return SubscribeMultipleMailboxRequest(mailboxId, listDescendantMailboxIds, subscribeState, subscribeAction); + if (mailboxNode == null) return null; + + if (mailboxNode.hasChildren()) { + final listDescendantMailboxIds = mailboxNode.descendantsAsList().mailboxIds; + log("BaseMailboxController::_generateSubscribeRequestWhenSubscribeDisabled:listDescendantMailboxIds $listDescendantMailboxIds"); + return SubscribeMultipleMailboxRequest( + mailboxId, + listDescendantMailboxIds, + MailboxSubscribeState.disabled, + subscribeAction + ); + } else { + return SubscribeMailboxRequest( + mailboxId, + MailboxSubscribeState.disabled, + subscribeAction + ); + } + } + + SubscribeRequest? _generateSubscribeRequestWhenSubscribeEnabled( + MailboxId mailboxId, + MailboxSubscribeAction subscribeAction + ) { + final mailboxNode = findMailboxNodeById(mailboxId); + + if (mailboxNode == null) return null; + + if (mailboxNode.hasParents()) { + final listAncestorMailboxIds = getAncestorOfMailboxNode(mailboxNode).mailboxIds; + listAncestorMailboxIds.add(mailboxId); + log("BaseMailboxController::_generateSubscribeRequestWhenSubscribeEnabled:listAncestorMailboxIds $listAncestorMailboxIds"); + if (listAncestorMailboxIds.isNotEmpty) { + return SubscribeMultipleMailboxRequest( + mailboxId, + listAncestorMailboxIds, + MailboxSubscribeState.enabled, + subscribeAction + ); } else { - return SubscribeMailboxRequest(mailboxId, subscribeState, subscribeAction); + return SubscribeMailboxRequest( + mailboxId, + MailboxSubscribeState.enabled, + subscribeAction + ); } + } else { + return SubscribeMailboxRequest( + mailboxId, + MailboxSubscribeState.enabled, + subscribeAction + ); } - return null; } void getAllMailbox(Session session, AccountId accountId) async { diff --git a/lib/features/base/isolate/isolate_manager.dart b/lib/features/base/isolate/isolate_manager.dart new file mode 100644 index 0000000000..f85c79dc63 --- /dev/null +++ b/lib/features/base/isolate/isolate_manager.dart @@ -0,0 +1,46 @@ + +import 'dart:async'; +import 'dart:isolate'; + +import 'package:core/presentation/utils/shims/dart_ui_real.dart'; +import 'package:core/utils/app_logger.dart'; + +abstract class IsolateManager { + + final _eventReceivePort = ReceivePort(); + StreamSubscription? _streamSubscription; + + String get isolateIdentityName; + + void initial({ + Function(dynamic)? onData, + Function? onError, + Function()? onDone, + }) { + try { + IsolateNameServer.registerPortWithName(_eventReceivePort.sendPort, isolateIdentityName); + _streamSubscription = _eventReceivePort.listen(onData, onError: onError, onDone: onDone); + } catch (e) { + logError('IsolateManager::initial():EXCEPTION: $e'); + } + } + + void addEvent(Object? value) { + log('IsolateManager::addEvent():value: $value'); + try { + final sendPort = IsolateNameServer.lookupPortByName(isolateIdentityName); + if (sendPort != null) { + sendPort.send(value); + } else { + logError('IsolateManager::addEvent(): sendPort is null'); + } + } catch (e) { + logError('IsolateManager::addEvent():EXCEPTION: $e'); + } + } + + void release() { + _eventReceivePort.close(); + _streamSubscription?.cancel(); + } +} \ No newline at end of file diff --git a/lib/features/base/mixin/date_range_picker_mixin.dart b/lib/features/base/mixin/date_range_picker_mixin.dart new file mode 100644 index 0000000000..78ff97a84a --- /dev/null +++ b/lib/features/base/mixin/date_range_picker_mixin.dart @@ -0,0 +1,71 @@ + +import 'package:core/presentation/utils/app_toast.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_date_range_picker/material_date_range_picker_dialog.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +mixin DateRangePickerMixin { + + void showMultipleViewDateRangePicker( + BuildContext context, + DateTime? initStartDate, + DateTime? initEndDate, + {Function(DateTime? startDate, DateTime? endDate)? onCallbackAction} + ) { + MaterialDateRangePickerDialog.showDateRangePicker( + context, + confirmText: AppLocalizations.of(context).setDate, + cancelText: AppLocalizations.of(context).cancel, + last7daysTitle: AppLocalizations.of(context).last7Days, + last30daysTitle: AppLocalizations.of(context).last30Days, + last6monthsTitle: AppLocalizations.of(context).last6Months, + lastYearTitle: AppLocalizations.of(context).lastYears, + initStartDate: initStartDate, + initEndDate: initEndDate, + autoClose: false, + selectDateRangeActionCallback: (startDate, endDate) { + _handleSelectDateRangeResult( + context, + startDate, + endDate, + onCallbackAction: onCallbackAction + ); + } + ); + } + + void _handleSelectDateRangeResult( + BuildContext context, + DateTime? startDate, + DateTime? endDate, + {Function(DateTime? startDate, DateTime? endDate)? onCallbackAction} + ) { + final appToast = Get.find(); + + if (startDate == null) { + appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).toastMessageErrorWhenSelectStartDateIsEmpty); + return; + } + + if (endDate == null) { + appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).toastMessageErrorWhenSelectEndDateIsEmpty); + return; + } + + if (endDate.isBefore(startDate)) { + appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).toastMessageErrorWhenSelectDateIsInValid); + return; + } + + popBack(); + onCallbackAction?.call(startDate, endDate); + } +} \ No newline at end of file diff --git a/lib/features/base/mixin/mailbox_action_handler_mixin.dart b/lib/features/base/mixin/mailbox_action_handler_mixin.dart index 2079d8599a..abce8c6b07 100644 --- a/lib/features/base/mixin/mailbox_action_handler_mixin.dart +++ b/lib/features/base/mixin/mailbox_action_handler_mixin.dart @@ -1,22 +1,26 @@ -import 'package:core/utils/app_logger.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart'; +import 'package:core/presentation/views/dialog/confirmation_dialog_builder.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; -import 'package:tmail_ui_user/main/routes/app_routes.dart'; -import 'package:tmail_ui_user/main/routes/navigation_router.dart'; -import 'package:tmail_ui_user/main/routes/route_utils.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; mixin MailboxActionHandlerMixin { void openMailboxInNewTabAction(PresentationMailbox mailbox) { - final mailboxRouteWeb = RouteUtils.generateRouteBrowser( - AppRoutes.dashboard, - NavigationRouter(mailboxId: mailbox.id) - ); - log('MailboxActionHandlerMixin::openMailboxInNewTabAction(): mailboxRouteWeb: $mailboxRouteWeb'); - AppUtils.launchLink(mailboxRouteWeb.toString()); + AppUtils.launchLink(mailbox.mailboxRouteWeb.toString()); } void markAsReadMailboxAction( @@ -27,19 +31,136 @@ mixin MailboxActionHandlerMixin { Function(BuildContext)? onCallbackAction } ) { + final session = dashboardController.sessionCurrent; final accountId = dashboardController.accountId.value; final mailboxId = presentationMailbox.id; - final mailboxName = presentationMailbox.name; final countEmailsUnread = presentationMailbox.unreadEmails?.value.value ?? 0; - if (accountId != null && mailboxName != null) { + if (session != null && accountId != null) { dashboardController.markAsReadMailbox( + session, accountId, mailboxId, - mailboxName, + presentationMailbox.getDisplayName(context), countEmailsUnread.toInt() ); onCallbackAction?.call(context); } } + + void emptyTrashAction( + BuildContext context, + PresentationMailbox mailbox, + MailboxDashBoardController dashboardController + ) { + final responsiveUtils = Get.find(); + final imagePaths = Get.find(); + final appToast = Get.find(); + + if (responsiveUtils.isScreenWithShortestSide(context)) { + (ConfirmationDialogActionSheetBuilder(context) + ..messageText(AppLocalizations.of(context).emptyTrashMessageDialog) + ..onCancelAction(AppLocalizations.of(context).cancel, popBack) + ..onConfirmAction(AppLocalizations.of(context).delete, () { + popBack(); + if (mailbox.countTotalEmails > 0) { + dashboardController.emptyTrashFolderAction(trashFolderId: mailbox.id); + } else { + appToast.showToastWarningMessage( + context, + AppLocalizations.of(context).noEmailInYourCurrentMailbox + ); + } + })) + .show(); + } else { + showDialog( + context: context, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + builder: (context) => PointerInterceptor(child: (ConfirmDialogBuilder(imagePaths) + ..key(const Key('confirm_dialog_empty_trash')) + ..title(AppLocalizations.of(context).emptyTrash) + ..content(AppLocalizations.of(context).emptyTrashMessageDialog) + ..addIcon(SvgPicture.asset(imagePaths.icRemoveDialog, fit: BoxFit.fill)) + ..colorConfirmButton(AppColor.colorConfirmActionDialog) + ..styleTextConfirmButton(const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: AppColor.colorActionDeleteConfirmDialog)) + ..onCloseButtonAction(popBack) + ..onConfirmButtonAction(AppLocalizations.of(context).delete, () { + popBack(); + if (mailbox.countTotalEmails > 0) { + dashboardController.emptyTrashFolderAction(trashFolderId: mailbox.id); + } else { + appToast.showToastWarningMessage( + context, + AppLocalizations.of(context).noEmailInYourCurrentMailbox + ); + } + }) + ..onCancelButtonAction(AppLocalizations.of(context).cancel, popBack)) + .build())); + } + } + + void emptySpamAction( + BuildContext context, + PresentationMailbox mailbox, + MailboxDashBoardController dashboardController + ) { + if (dashboardController.isDrawerOpen) { + dashboardController.closeMailboxMenuDrawer(); + } + + final responsiveUtils = Get.find(); + final imagePaths = Get.find(); + final appToast = Get.find(); + + if (responsiveUtils.isScreenWithShortestSide(context)) { + (ConfirmationDialogActionSheetBuilder(context) + ..messageText(AppLocalizations.of(context).emptySpamMessageDialog) + ..onCancelAction(AppLocalizations.of(context).cancel, popBack) + ..onConfirmAction(AppLocalizations.of(context).delete_all, () { + popBack(); + if (mailbox.countTotalEmails > 0) { + dashboardController.emptySpamFolderAction(spamFolderId: mailbox.id); + } else { + appToast.showToastWarningMessage( + context, + AppLocalizations.of(context).noEmailInYourCurrentMailbox + ); + } + })) + .show(); + } else { + showDialog( + context: context, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + builder: (context) => PointerInterceptor(child: (ConfirmDialogBuilder(imagePaths) + ..key(const Key('confirm_dialog_empty_spam')) + ..title(AppLocalizations.of(context).emptySpamFolder) + ..content(AppLocalizations.of(context).emptySpamMessageDialog) + ..addIcon(SvgPicture.asset(imagePaths.icRemoveDialog, fit: BoxFit.fill)) + ..colorConfirmButton(AppColor.colorConfirmActionDialog) + ..styleTextConfirmButton(const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: AppColor.colorActionDeleteConfirmDialog)) + ..onCloseButtonAction(popBack) + ..onConfirmButtonAction(AppLocalizations.of(context).delete_all, () { + popBack(); + if (mailbox.countTotalEmails > 0) { + dashboardController.emptySpamFolderAction(spamFolderId: mailbox.id); + } else { + appToast.showToastWarningMessage( + context, + AppLocalizations.of(context).noEmailInYourCurrentMailbox + ); + } + }) + ..onCancelButtonAction(AppLocalizations.of(context).cancel, popBack) + ).build())); + } + } } \ No newline at end of file diff --git a/lib/features/base/mixin/message_dialog_action_mixin.dart b/lib/features/base/mixin/message_dialog_action_mixin.dart index 77b07040fe..7c83643297 100644 --- a/lib/features/base/mixin/message_dialog_action_mixin.dart +++ b/lib/features/base/mixin/message_dialog_action_mixin.dart @@ -14,37 +14,78 @@ mixin MessageDialogActionMixin { String actionName, { Function? onConfirmAction, + Function? onCancelAction, String? title, String? cancelTitle, bool hasCancelButton = true, bool showAsBottomSheet = false, + bool alignCenter = false, + List? listTextSpan, Widget? icon, TextStyle? titleStyle, + TextStyle? messageStyle, TextStyle? actionStyle, TextStyle? cancelStyle, Color? actionButtonColor, Color? cancelButtonColor, } - ) { - final _responsiveUtils = Get.find(); - final _imagePaths = Get.find(); + ) async { + final responsiveUtils = Get.find(); + final imagePaths = Get.find(); - if (_responsiveUtils.isMobile(context)) { - if (showAsBottomSheet) { - showModalBottomSheet( + if (alignCenter) { + await showDialog( + context: context, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + builder: (BuildContext context) => PointerInterceptor( + child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan, heightButton: 44) + ..key(const Key('confirm_dialog_action')) + ..title(title ?? '') + ..content(message) + ..addIcon(icon) + ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) + ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) + ..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null) + ..paddingTitle(icon != null ? const EdgeInsets.only(top: 24) : EdgeInsets.zero) + ..radiusButton(12) + ..paddingContent(const EdgeInsets.only(left: 24, right: 24, bottom: 24, top: 12)) + ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 24, left: 24, right: 24)) + ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) + ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) + ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) + ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) + ..onConfirmButtonAction(actionName, () { + popBack(); + onConfirmAction?.call(); + }) + ..onCancelButtonAction( + hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', + () { + popBack(); + onCancelAction?.call(); + } + ) + ).build() + ) + ); + } else { + if (responsiveUtils.isMobile(context)) { + if (showAsBottomSheet) { + await showModalBottomSheet( context: context, isScrollControlled: true, barrierColor: AppColor.colorDefaultCupertinoActionSheet, backgroundColor: Colors.transparent, enableDrag: true, builder: (BuildContext context) => PointerInterceptor( - child: (ConfirmDialogBuilder(_imagePaths, showAsBottomSheet: true) + child: (ConfirmDialogBuilder(imagePaths, showAsBottomSheet: true, listTextSpan: listTextSpan) ..key(const Key('confirm_dialog_action')) ..title(title ?? '') ..content(message) ..addIcon(icon) ..margin(const EdgeInsets.symmetric(vertical: 42, horizontal: 16)) - ..widthDialog(_responsiveUtils.getSizeScreenWidth(context)) + ..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null) + ..widthDialog(responsiveUtils.getSizeScreenWidth(context)) ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) ..paddingTitle(icon != null ? const EdgeInsets.only(top: 24) : EdgeInsets.zero) @@ -52,52 +93,77 @@ mixin MessageDialogActionMixin { ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) - ..styleContent(const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) + ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) ..onConfirmButtonAction(actionName, () { - popBack(); - onConfirmAction?.call(); + popBack(); + onConfirmAction?.call(); }) - ..onCancelButtonAction(hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', () => popBack()) + ..onCancelButtonAction( + hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', + () { + popBack(); + onCancelAction?.call(); + } + ) ..onCloseButtonAction(() => popBack())) .build())); + } else { + (ConfirmationDialogActionSheetBuilder(context, listTextSpan: listTextSpan) + ..messageText(message) + ..styleConfirmButton(const TextStyle(fontSize: 20, fontWeight: FontWeight.normal, color: Colors.black)) + ..styleMessage(messageStyle) + ..styleCancelButton(cancelStyle) + ..onCancelAction( + cancelTitle ?? AppLocalizations.of(context).cancel, + () { + popBack(); + onCancelAction?.call(); + } + ) + ..onConfirmAction(actionName, () { + popBack(); + onConfirmAction?.call(); + })).show(); + } } else { - (ConfirmationDialogActionSheetBuilder(context) - ..messageText(message) - ..styleConfirmButton(const TextStyle(fontSize: 20, fontWeight: FontWeight.normal, color: Colors.black)) - ..onCancelAction(cancelTitle ?? AppLocalizations.of(context).cancel, () => popBack()) - ..onConfirmAction(actionName, () { - popBack(); - onConfirmAction?.call(); - })).show(); - } - } else { - showDialog( + await showDialog( context: context, barrierColor: AppColor.colorDefaultCupertinoActionSheet, - builder: (BuildContext context) => PointerInterceptor(child: (ConfirmDialogBuilder(_imagePaths) + builder: (BuildContext context) => PointerInterceptor( + child: (ConfirmDialogBuilder(imagePaths, listTextSpan: listTextSpan) ..key(const Key('confirm_dialog_action')) ..title(title ?? '') ..content(message) ..addIcon(icon) ..colorConfirmButton(actionButtonColor ?? AppColor.colorTextButton) ..colorCancelButton(cancelButtonColor ?? AppColor.colorCancelButton) + ..marginIcon(icon != null ? const EdgeInsets.only(top: 24) : null) ..paddingTitle(icon != null ? const EdgeInsets.only(top: 24) : EdgeInsets.zero) ..marginIcon(EdgeInsets.zero) ..paddingContent(const EdgeInsets.only(left: 44, right: 44, bottom: 24, top: 12)) ..paddingButton(hasCancelButton ? null : const EdgeInsets.only(bottom: 16, left: 44, right: 44)) ..styleTitle(titleStyle ?? const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)) - ..styleContent(const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) + ..styleContent(messageStyle ?? const TextStyle(fontSize: 14, fontWeight: FontWeight.normal, color: AppColor.colorContentEmail)) ..styleTextCancelButton(cancelStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorTextButton)) ..styleTextConfirmButton(actionStyle ?? const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: Colors.white)) ..onConfirmButtonAction(actionName, () { popBack(); onConfirmAction?.call(); }) - ..onCancelButtonAction(hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', () => popBack()) + ..onCancelButtonAction( + hasCancelButton ? cancelTitle ?? AppLocalizations.of(context).cancel : '', + () { + popBack(); + onCancelAction?.call(); + } + ) ..onCloseButtonAction(() => popBack())) - .build())); + .build() + ) + ); + } } } } \ No newline at end of file diff --git a/lib/features/base/mixin/popup_context_menu_action_mixin.dart b/lib/features/base/mixin/popup_context_menu_action_mixin.dart index cff30589d4..e3ddb014e2 100644 --- a/lib/features/base/mixin/popup_context_menu_action_mixin.dart +++ b/lib/features/base/mixin/popup_context_menu_action_mixin.dart @@ -1,5 +1,7 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/bottom_popup/cupertino_action_sheet_builder.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -14,19 +16,29 @@ mixin PopupContextMenuActionMixin { .show(); } - void openPopupMenuAction(BuildContext context, RelativeRect? position, List popupMenuItems) async { + void openPopupMenuAction( + BuildContext context, + RelativeRect? position, + List popupMenuItems, + { + double? radius, + } + ) async { await showMenu( - context: context, - position: position ?? const RelativeRect.fromLTRB(16, 40, 16, 16), - color: Colors.white, - elevation: 4, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - items: popupMenuItems); + context: context, + position: position ?? const RelativeRect.fromLTRB(16, 40, 16, 16), + color: Colors.white, + elevation: 4, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(radius ?? 16)) + ), + items: popupMenuItems + ); } Widget buildCancelButton(BuildContext context) { return MouseRegion( - cursor: BuildUtils.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( child: Text( AppLocalizations.of(context).cancel, diff --git a/lib/features/base/mixin/popup_menu_widget_mixin.dart b/lib/features/base/mixin/popup_menu_widget_mixin.dart index a6f08d1573..507f92c7ed 100644 --- a/lib/features/base/mixin/popup_menu_widget_mixin.dart +++ b/lib/features/base/mixin/popup_menu_widget_mixin.dart @@ -1,4 +1,5 @@ +import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -24,7 +25,7 @@ mixin PopupMenuWidgetMixin { width: iconSize ?? 20, height: iconSize ?? 20, fit: BoxFit.fill, - color: colorIcon + colorFilter: colorIcon.asFilter() ), const SizedBox(width: 12), Expanded(child: Text( diff --git a/lib/features/base/mixin/view_as_dialog_action_mixin.dart b/lib/features/base/mixin/view_as_dialog_action_mixin.dart deleted file mode 100644 index 413852b7a0..0000000000 --- a/lib/features/base/mixin/view_as_dialog_action_mixin.dart +++ /dev/null @@ -1,168 +0,0 @@ - -import 'package:flutter/material.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:tmail_ui_user/features/contact/presentation/contact_bindings.dart'; -import 'package:tmail_ui_user/features/contact/presentation/contact_view.dart'; -import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; -import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_bindings.dart'; -import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_view.dart'; -import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_bindings.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_view.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/model/identity_creator_arguments.dart'; -import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_bindings.dart'; -import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_view.dart'; -import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart'; -import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/new_mailbox_arguments.dart'; -import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart'; -import 'package:tmail_ui_user/features/rules_filter_creator/presentation/rules_filter_creator_bindings.dart'; -import 'package:tmail_ui_user/features/rules_filter_creator/presentation/rules_filter_creator_view.dart'; -import 'package:tmail_ui_user/main/routes/route_navigation.dart'; - -mixin ViewAsDialogActionMixin { - - void showDialogDestinationPicker({ - required BuildContext context, - required DestinationPickerArguments arguments, - required Function(PresentationMailbox) onSelectedMailbox - }) { - DestinationPickerBindings().dependencies(); - - showGeneralDialog( - context: context, - barrierDismissible: true, - barrierLabel: '', - barrierColor: Colors.black.withAlpha(24), - pageBuilder: (context, animation, secondaryAnimation) { - return DestinationPickerView.fromArguments( - arguments, - onDismissCallback: () { - DestinationPickerBindings().dispose(); - popBack(); - }, - onSelectedMailboxCallback: (destinationMailbox) { - DestinationPickerBindings().dispose(); - popBack(); - - if (destinationMailbox is PresentationMailbox) { - onSelectedMailbox.call(destinationMailbox); - } - }); - }); - } - - void showDialogMailboxCreator({ - required BuildContext context, - required MailboxCreatorArguments arguments, - required Function(NewMailboxArguments) onCreatedMailbox - }) { - MailboxCreatorBindings().dependencies(); - - showGeneralDialog( - context: context, - barrierDismissible: true, - barrierLabel: '', - barrierColor: Colors.black.withAlpha(24), - pageBuilder: (context, animation, secondaryAnimation) { - return MailboxCreatorView.fromArguments( - arguments, - onDismissCallback: () { - MailboxCreatorBindings().dispose(); - popBack(); - }, - onCreatedMailboxCallback: (arguments) { - MailboxCreatorBindings().dispose(); - popBack(); - - if (arguments is NewMailboxArguments) { - onCreatedMailbox.call(arguments); - } - }); - }); - } - - void showDialogContactView({ - required BuildContext context, - required ContactArguments arguments, - required Function(EmailAddress) onSelectedContact - }) { - ContactBindings().dependencies(); - - showGeneralDialog( - context: context, - barrierDismissible: true, - barrierLabel: '', - barrierColor: Colors.black.withAlpha(24), - pageBuilder: (context, animation, secondaryAnimation) { - return ContactView.fromArguments( - arguments, - onDismissCallback: () { - ContactBindings().dispose(); - popBack(); - }, - onSelectedContactCallback: (emailAddress) { - ContactBindings().dispose(); - popBack(); - - onSelectedContact.call(emailAddress); - }); - }); - } - - void showDialogIdentityCreator({ - required BuildContext context, - required IdentityCreatorArguments arguments, - required Function(dynamic) onCreatedIdentity - }) { - IdentityCreatorBindings().dependencies(); - - showGeneralDialog( - context: context, - barrierDismissible: true, - barrierLabel: '', - barrierColor: Colors.black.withAlpha(24), - pageBuilder: (context, animation, secondaryAnimation) { - return IdentityCreatorView.fromArguments( - arguments, - onDismissCallback: () { - IdentityCreatorBindings().dispose(); - popBack(); - }, - onCreatedIdentityCallback: (args) { - IdentityCreatorBindings().dispose(); - popBack(); - - onCreatedIdentity.call(args); - }); - }); - } - - void showDialogRuleFilterCreator({ - required BuildContext context, - required RulesFilterCreatorArguments arguments, - required Function(dynamic) onCreatedRuleFilter - }) { - RulesFilterCreatorBindings().dependencies(); - - showGeneralDialog( - context: context, - barrierDismissible: true, - barrierLabel: '', - barrierColor: Colors.black.withAlpha(24), - pageBuilder: (context, animation, secondaryAnimation) { - return RuleFilterCreatorView.fromArguments( - arguments, - onDismissCallback: () { - RulesFilterCreatorBindings().dispose(); - popBack(); - }, - onCreatedRuleFilterCallback: (args) { - RulesFilterCreatorBindings().dispose(); - popBack(); - - onCreatedRuleFilter.call(args); - }); - }); - } -} \ No newline at end of file diff --git a/lib/features/base/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart index 8c76b03b8d..a08e78b977 100644 --- a/lib/features/base/reloadable/reloadable_controller.dart +++ b/lib/features/base/reloadable/reloadable_controller.dart @@ -2,109 +2,62 @@ import 'package:core/data/network/config/dynamic_url_interceptors.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:dartz/dartz.dart'; -import 'package:fcm/model/firebase_capability.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:model/account/authentication_type.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/extensions/session_extension.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; -import 'package:tmail_ui_user/features/caching/caching_manager.dart'; -import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_authenticated_account_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_credential_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_state.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart'; -import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/state/log_out_oidc_state.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_interactors_bindings.dart'; -import 'package:tmail_ui_user/features/push_notification/domain/state/destroy_subscription_state.dart'; -import 'package:tmail_ui_user/features/push_notification/domain/state/get_fcm_subscription_local.dart'; -import 'package:tmail_ui_user/features/push_notification/domain/usecases/destroy_subscription_interactor.dart'; -import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_fcm_subscription_local_interactor.dart'; -import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_receiver.dart'; +import 'package:tmail_ui_user/features/session/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/session/domain/state/get_session_state.dart'; import 'package:tmail_ui_user/features/session/domain/usecases/get_session_interactor.dart'; -import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; -import 'package:tmail_ui_user/main/routes/app_routes.dart'; -import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -import 'package:tmail_ui_user/main/utils/app_config.dart'; abstract class ReloadableController extends BaseController { - final DynamicUrlInterceptors _dynamicUrlInterceptors = Get.find(); - final AuthorizationInterceptors _authorizationInterceptors = Get.find(); - final AuthorizationInterceptors _authorizationIsolateInterceptors = Get.find(tag: BindingTag.isolateTag); + final DynamicUrlInterceptors dynamicUrlInterceptors = Get.find(); final GetSessionInteractor _getSessionInteractor = Get.find(); - final DeleteCredentialInteractor _deleteCredentialInteractor = Get.find(); - final CachingManager _cachingManager = Get.find(); - final _languageCacheManager = Get.find(); - - final LogoutOidcInteractor _logoutOidcInteractor; - final DeleteAuthorityOidcInteractor _deleteAuthorityOidcInteractor; final GetAuthenticatedAccountInteractor _getAuthenticatedAccountInteractor; final UpdateAuthenticationAccountInteractor _updateAuthenticationAccountInteractor; - final _fcmReceiver = FcmReceiver.instance; - - GetFCMSubscriptionLocalInteractor? _getSubscriptionLocalInteractor; - DestroySubscriptionInteractor? _destroySubscriptionInteractor; - bool _isFcmEnabled = false; ReloadableController( - this._logoutOidcInteractor, - this._deleteAuthorityOidcInteractor, this._getAuthenticatedAccountInteractor, this._updateAuthenticationAccountInteractor ); @override - void onData(Either newState) { - super.onData(newState); - viewState.value.fold( - (failure) { - if (failure is GetCredentialFailure) { - _goToLogin(arguments: LoginArguments(LoginFormType.credentialForm)); - } else if (failure is GetSessionFailure) { - _handleGetSessionFailure(); - } else if (failure is LogoutOidcFailure) { - log('ReloadableController::onData(): LogoutOidcFailure: $failure'); - _getSubscriptionLocalAction(); - } else if (failure is GetStoredTokenOidcFailure) { - _goToLogin(arguments: LoginArguments(LoginFormType.ssoForm)); - } else if (failure is GetAuthenticatedAccountFailure || failure is NoAuthenticatedAccountFailure) { - _goToLogin(arguments: LoginArguments(LoginFormType.credentialForm)); - } else if (failure is GetFCMSubscriptionLocalFailure) { - _checkAuthenticationTypeWhenLogout(); - } else if (failure is DestroySubscriptionFailure) { - _checkAuthenticationTypeWhenLogout(); - } - }, - (success) { - if (success is GetCredentialViewState) { - _handleGetCredentialSuccess(success); - } else if (success is GetSessionSuccess) { - _handleGetSessionSuccess(success); - } else if (success is LogoutOidcSuccess) { - log('ReloadableController::handleLogoutOidcSuccess(): $success'); - _getSubscriptionLocalAction(); - } else if (success is GetStoredTokenOidcSuccess) { - _handleGetStoredTokenOIDCSuccess(success); - } else if (success is GetFCMSubscriptionLocalSuccess) { - final _subscriptionId = success.fcmSubscription.subscriptionId; - _destroySubscriptionAction(_subscriptionId); - } else if (success is DestroySubscriptionSuccess) { - _checkAuthenticationTypeWhenLogout(); - } - } - ); + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is GetCredentialFailure) { + goToLogin(arguments: LoginArguments(LoginFormType.credentialForm)); + } else if (failure is GetSessionFailure) { + _handleGetSessionFailure(); + } else if (failure is GetStoredTokenOidcFailure) { + goToLogin(arguments: LoginArguments(LoginFormType.ssoForm)); + } else if (failure is GetAuthenticatedAccountFailure) { + goToLogin(arguments: LoginArguments(LoginFormType.credentialForm)); + } + } + + @override + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetCredentialViewState) { + _handleGetCredentialSuccess(success); + } else if (success is GetSessionSuccess) { + _handleGetSessionSuccess(success); + } else if (success is GetStoredTokenOidcSuccess) { + _handleGetStoredTokenOIDCSuccess(success); + } } /* @@ -118,18 +71,15 @@ abstract class ReloadableController extends BaseController { consumeState(_getAuthenticatedAccountInteractor.execute()); } - void _goToLogin({LoginArguments? arguments}) { - pushAndPopAll(AppRoutes.login, arguments: arguments); - } - void _setUpInterceptors(GetCredentialViewState credentialViewState) { - _dynamicUrlInterceptors.changeBaseUrl(credentialViewState.baseUrl.origin); - _authorizationInterceptors.setBasicAuthorization( - credentialViewState.userName.userName, + dynamicUrlInterceptors.setJmapUrl(credentialViewState.baseUrl.origin); + dynamicUrlInterceptors.changeBaseUrl(credentialViewState.baseUrl.origin); + authorizationInterceptors.setBasicAuthorization( + credentialViewState.userName.value, credentialViewState.password.value, ); - _authorizationIsolateInterceptors.setBasicAuthorization( - credentialViewState.userName.userName, + authorizationIsolateInterceptors.setBasicAuthorization( + credentialViewState.userName.value, credentialViewState.password.value, ); } @@ -140,79 +90,42 @@ abstract class ReloadableController extends BaseController { } void _getSessionAction() { - consumeState(_getSessionInteractor.execute().asStream()); + consumeState(_getSessionInteractor.execute()); } - void _handleGetSessionFailure() async { - await Future.wait([ - _deleteCredentialInteractor.execute(), - _deleteAuthorityOidcInteractor.execute(), - _cachingManager.clearAll(), - _languageCacheManager.removeLanguage(), - ]); - final authenticationType = _authorizationInterceptors.authenticationType; - if (authenticationType == AuthenticationType.oidc) { - _goToLogin(arguments: LoginArguments(LoginFormType.ssoForm)); - } else { - _goToLogin(arguments: LoginArguments(LoginFormType.credentialForm)); - } + void _handleGetSessionFailure() { + performInvokeLogoutAction(); } void _handleGetSessionSuccess(GetSessionSuccess success) { - final apiUrl = success.session.apiUrl.toString(); + final session = success.session; + final personalAccount = session.personalAccount; + final apiUrl = session.getQualifiedApiUrl(baseUrl: dynamicUrlInterceptors.jmapUrl); + log('ReloadableController::_handleGetSessionSuccess():apiUrl: $apiUrl'); if (apiUrl.isNotEmpty) { - _dynamicUrlInterceptors.changeBaseUrl(apiUrl); - updateAuthenticationAccount(success.session, success.session.accounts.keys.first); - handleReloaded(success.session); + dynamicUrlInterceptors.changeBaseUrl(apiUrl); + updateAuthenticationAccount(session, personalAccount.accountId, session.username); + handleReloaded(session); } else { - _handleGetSessionFailure(); + logError('ReloadableController::_handleGetSessionSuccess(): apiUrl is NULL'); + performInvokeLogoutAction(); } } void handleReloaded(Session session) {} - void logoutAction() async { - await Future.wait([ - _deleteCredentialInteractor.execute(), - _cachingManager.clearAll(), - _languageCacheManager.removeLanguage(), - ]); - _authorizationInterceptors.clear(); - _authorizationIsolateInterceptors.clear(); - if (_isFcmEnabled) { - _fcmReceiver.deleteFcmToken(); - } - await _cachingManager.closeHive(); - _goToLogin(arguments: LoginArguments(LoginFormType.credentialForm)); - } - - void _logoutOIDCAction() async { - log('ReloadableController::_logoutOIDCAction():'); - await Future.wait([ - _deleteAuthorityOidcInteractor.execute(), - _cachingManager.clearAll(), - _languageCacheManager.removeLanguage(), - ]); - _authorizationIsolateInterceptors.clear(); - _authorizationInterceptors.clear(); - if (_isFcmEnabled) { - _fcmReceiver.deleteFcmToken(); - } - await _cachingManager.closeHive(); - _goToLogin(arguments: LoginArguments(LoginFormType.ssoForm)); - } - void _handleGetStoredTokenOIDCSuccess(GetStoredTokenOidcSuccess tokenOidcSuccess) { _setUpInterceptorsOidc(tokenOidcSuccess); _getSessionAction(); } void _setUpInterceptorsOidc(GetStoredTokenOidcSuccess tokenOidcSuccess) { - _dynamicUrlInterceptors.changeBaseUrl(tokenOidcSuccess.baseUrl.toString()); - _authorizationInterceptors.setTokenAndAuthorityOidc( + dynamicUrlInterceptors.setJmapUrl(tokenOidcSuccess.baseUrl.toString()); + dynamicUrlInterceptors.changeBaseUrl(tokenOidcSuccess.baseUrl.toString()); + authorizationInterceptors.setTokenAndAuthorityOidc( newToken: tokenOidcSuccess.tokenOidc.toToken(), newConfig: tokenOidcSuccess.oidcConfiguration); - _authorizationIsolateInterceptors.setTokenAndAuthorityOidc( + authorizationIsolateInterceptors.setTokenAndAuthorityOidc( newToken: tokenOidcSuccess.tokenOidc.toToken(), newConfig: tokenOidcSuccess.oidcConfiguration); } @@ -226,71 +139,11 @@ abstract class ReloadableController extends BaseController { } } - Future _getSubscriptionLocalAction() { - try { - _getSubscriptionLocalInteractor = Get.find(); - consumeState(_getSubscriptionLocalInteractor!.execute()); - } catch (e) { - logError( - 'ReloadableController::getSubscriptionLocalAction(): exception: $e'); - logoutAction(); - } - return Future.value(); - } - - void _destroySubscriptionAction(String subscriptionId) { - try { - _destroySubscriptionInteractor = Get.find(); - consumeState(_destroySubscriptionInteractor!.execute(subscriptionId)); - } catch(e) { - logError('ReloadableController::destroySubscriptionAction(): exception: $e'); - logoutAction(); - } - } - - bool fcmEnabled(Session? session, AccountId? accountId) { - bool _fcmEnabled = false; - try { - requireCapability(session!, accountId!, [FirebaseCapability.fcmIdentifier]); - if (AppConfig.fcmAvailable) { - _fcmEnabled = true; - } else { - _fcmEnabled = false; - } - } catch (e) { - logError('BaseController::fcmEnabled(): exception: $e'); - } - return _fcmEnabled; - } - - void logout(Session? session, AccountId? accountId) { - _isFcmEnabled = fcmEnabled(session, accountId); - if (_isFcmEnabled) { - final authenticationType = _authorizationInterceptors.authenticationType; - if (authenticationType == AuthenticationType.oidc) { - consumeState(_logoutOidcInteractor.execute()); - } else { - _getSubscriptionLocalAction(); - } - } else { - _checkAuthenticationTypeWhenLogout(); - } - } - - void _checkAuthenticationTypeWhenLogout() { - final authenticationType = _authorizationInterceptors.authenticationType; - if (authenticationType == AuthenticationType.oidc) { - _logoutOIDCAction(); - } else { - logoutAction(); - } - } - - void updateAuthenticationAccount(Session? session, AccountId? accountId) { - final apiUrl = session?.apiUrl.toString() ?? ''; + void updateAuthenticationAccount(Session session, AccountId accountId, UserName userName) { + final apiUrl = session.getQualifiedApiUrl(baseUrl: dynamicUrlInterceptors.jmapUrl); log('ReloadableController::updateAuthenticationAccount():apiUrl: $apiUrl'); - if (accountId != null && apiUrl.isNotEmpty) { - consumeState(_updateAuthenticationAccountInteractor.execute(accountId, apiUrl)); + if (apiUrl.isNotEmpty) { + consumeState(_updateAuthenticationAccountInteractor.execute(accountId, apiUrl, userName)); } } } \ No newline at end of file diff --git a/lib/features/base/state/button_state.dart b/lib/features/base/state/button_state.dart new file mode 100644 index 0000000000..dabb01f270 --- /dev/null +++ b/lib/features/base/state/button_state.dart @@ -0,0 +1,14 @@ + +enum ButtonState { + enabled, + disabled; + + double get opacity { + switch(this) { + case ButtonState.enabled: + return 1.0; + case ButtonState.disabled: + return 0.4; + } + } +} \ No newline at end of file diff --git a/lib/features/base/state/ui_action_state.dart b/lib/features/base/state/ui_action_state.dart index d53d19d016..10cd8953df 100644 --- a/lib/features/base/state/ui_action_state.dart +++ b/lib/features/base/state/ui_action_state.dart @@ -2,10 +2,10 @@ import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -class UIActionState extends UIState { +abstract class UIActionState extends UIState { - jmap.State? currentEmailState; - jmap.State? currentMailboxState; + final jmap.State? currentEmailState; + final jmap.State? currentMailboxState; UIActionState(this.currentEmailState, this.currentMailboxState); diff --git a/lib/features/base/styles/circle_loading_widget_styles.dart b/lib/features/base/styles/circle_loading_widget_styles.dart new file mode 100644 index 0000000000..f8544f381a --- /dev/null +++ b/lib/features/base/styles/circle_loading_widget_styles.dart @@ -0,0 +1,8 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class CircleLoadingWidgetStyles { + static const Color progressColor = AppColor.primaryColor; + static const double size = 24; + static const double width = 2; +} \ No newline at end of file diff --git a/lib/features/base/styles/cupertino_loading_widget_styles.dart b/lib/features/base/styles/cupertino_loading_widget_styles.dart new file mode 100644 index 0000000000..3000ad3694 --- /dev/null +++ b/lib/features/base/styles/cupertino_loading_widget_styles.dart @@ -0,0 +1,7 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class CupertinoLoadingWidgetStyles { + static const Color progressColor = AppColor.colorLoading; + static const double size = 24; +} \ No newline at end of file diff --git a/lib/features/base/styles/hyper_link_widget_styles.dart b/lib/features/base/styles/hyper_link_widget_styles.dart new file mode 100644 index 0000000000..c8668cb476 --- /dev/null +++ b/lib/features/base/styles/hyper_link_widget_styles.dart @@ -0,0 +1,7 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class HyperLinkWidgetStyles { + static const Color textColor = AppColor.primaryColor; + static const double textSize = 16; +} \ No newline at end of file diff --git a/lib/features/base/styles/popup_item_widget_style.dart b/lib/features/base/styles/popup_item_widget_style.dart new file mode 100644 index 0000000000..b7317eecb4 --- /dev/null +++ b/lib/features/base/styles/popup_item_widget_style.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class PopupItemWidgetStyle { + static const double iconSize = 24; + static const double selectedIconSize = 16; + static const double space = 12; + static const double height = 48; + static const double minWidth = 256; + + static const EdgeInsetsGeometry padding = EdgeInsets.symmetric(horizontal: 20, vertical: 16); + static const EdgeInsetsGeometry iconSelectedPadding = EdgeInsetsDirectional.only(start: 12); + + static const TextStyle labelTextStyle = TextStyle( + fontSize: 17, + fontWeight: FontWeight.normal, + color: Colors.black + ); +} \ No newline at end of file diff --git a/lib/features/base/widget/circle_loading_widget.dart b/lib/features/base/widget/circle_loading_widget.dart new file mode 100644 index 0000000000..5b635d1849 --- /dev/null +++ b/lib/features/base/widget/circle_loading_widget.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/styles/circle_loading_widget_styles.dart'; + +class CircleLoadingWidget extends StatelessWidget { + + final double? size; + final EdgeInsetsGeometry? padding; + + const CircleLoadingWidget({super.key, this.size, this.padding}); + + @override + Widget build(BuildContext context) { + if (padding != null) { + return Padding( + padding: padding!, + child: SizedBox( + width: size ?? CircleLoadingWidgetStyles.size, + height: size ?? CircleLoadingWidgetStyles.size, + child: const CircularProgressIndicator( + color: CircleLoadingWidgetStyles.progressColor, + strokeWidth: CircleLoadingWidgetStyles.width, + ) + ), + ); + } else { + return SizedBox( + width: size ?? CircleLoadingWidgetStyles.size, + height: size ?? CircleLoadingWidgetStyles.size, + child: const CircularProgressIndicator( + color: CircleLoadingWidgetStyles.progressColor, + strokeWidth: CircleLoadingWidgetStyles.width, + ) + ); + } + } +} \ No newline at end of file diff --git a/lib/features/base/widget/compose_floating_button.dart b/lib/features/base/widget/compose_floating_button.dart new file mode 100644 index 0000000000..6d1d3c9d60 --- /dev/null +++ b/lib/features/base/widget/compose_floating_button.dart @@ -0,0 +1,56 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/floating_button/scrolling_floating_button_animated.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class ComposeFloatingButton extends StatelessWidget { + + final ScrollController scrollController; + final VoidCallback? onTap; + + const ComposeFloatingButton({ + super.key, + required this.scrollController, + this.onTap + }); + + @override + Widget build(BuildContext context) { + final imagePaths = getBinding(); + + return Align( + alignment: AlignmentDirectional.bottomEnd, + child: ScrollingFloatingButtonAnimated( + icon: SvgPicture.asset( + imagePaths!.icComposeWeb, + width: 28, + height: 28, + fit: BoxFit.fill + ), + text: Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: Text(AppLocalizations.of(context).compose, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: const TextStyle( + color: Colors.white, + fontSize: 16.0, + fontWeight: FontWeight.w500 + ) + ) + ), + onPress: onTap, + scrollController: scrollController, + color: AppColor.primaryColor, + width: 154, + height: 60, + animateIcon: false + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/base/widget/cupertino_loading_widget.dart b/lib/features/base/widget/cupertino_loading_widget.dart new file mode 100644 index 0000000000..52a4c4a2bf --- /dev/null +++ b/lib/features/base/widget/cupertino_loading_widget.dart @@ -0,0 +1,38 @@ +import 'package:flutter/cupertino.dart'; +import 'package:tmail_ui_user/features/base/styles/cupertino_loading_widget_styles.dart'; + +class CupertinoLoadingWidget extends StatelessWidget { + + final double? size; + final EdgeInsetsGeometry? padding; + + const CupertinoLoadingWidget({super.key, this.size, this.padding}); + + @override + Widget build(BuildContext context) { + if (padding != null) { + return Padding( + padding: padding!, + child: Center( + child: SizedBox( + width: size ?? CupertinoLoadingWidgetStyles.size, + height: size ?? CupertinoLoadingWidgetStyles.size, + child: const CupertinoActivityIndicator( + color: CupertinoLoadingWidgetStyles.progressColor + ) + ) + ), + ); + } else { + return Center( + child: SizedBox( + width: size ?? CupertinoLoadingWidgetStyles.size, + height: size ?? CupertinoLoadingWidgetStyles.size, + child: const CupertinoActivityIndicator( + color: CupertinoLoadingWidgetStyles.progressColor + ) + ) + ); + } + } +} \ No newline at end of file diff --git a/lib/features/base/widget/custom_scroll_behavior.dart b/lib/features/base/widget/custom_scroll_behavior.dart new file mode 100644 index 0000000000..36180d7df5 --- /dev/null +++ b/lib/features/base/widget/custom_scroll_behavior.dart @@ -0,0 +1,10 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +class CustomScrollBehavior extends MaterialScrollBehavior { + @override + Set get dragDevices => { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + }; +} diff --git a/lib/features/base/widget/drop_down_button_widget.dart b/lib/features/base/widget/drop_down_button_widget.dart index 176d3e1ea9..dab5bbbb22 100644 --- a/lib/features/base/widget/drop_down_button_widget.dart +++ b/lib/features/base/widget/drop_down_button_widget.dart @@ -3,15 +3,14 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; -import 'package:enough_html_editor/enough_html_editor.dart' as enough_html_editor; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:rich_text_composer/rich_text_composer.dart' as rich_text_composer; import 'package:rule_filter/rule_filter/rule_condition.dart' as rule_condition; import 'package:tmail_ui_user/features/composer/presentation/model/font_name_type.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/extensions/locale_extension.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/extensions/rule_condition_extensions.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/email_rule_filter_action.dart'; @@ -28,7 +27,7 @@ class DropDownButtonWidget extends StatelessWidget { final double radiusButton; final double opacity; final Widget? iconArrowDown; - final Color? colorButton; + final Color colorButton; final String tooltip; final double? dropdownWidth; final double? dropdownMaxHeight; @@ -36,7 +35,7 @@ class DropDownButtonWidget extends StatelessWidget { const DropDownButtonWidget({ Key? key, required this.items, - required this.itemSelected, + this.itemSelected, this.onChanged, this.onMenuStateChange, this.supportHint = false, @@ -54,7 +53,7 @@ class DropDownButtonWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final _imagePaths = Get.find(); + final imagePaths = Get.find(); return DropdownButtonHideUnderline( child: PointerInterceptor( @@ -91,7 +90,7 @@ class DropDownButtonWidget extends StatelessWidget { overflow: CommonTextStyle.defaultTextOverFlow, )), if (supportSelectionIcon && item == itemSelected) - SvgPicture.asset(_imagePaths.icChecked, + SvgPicture.asset(imagePaths.icChecked, width: sizeIconChecked, height: sizeIconChecked, fit: BoxFit.fill) @@ -112,7 +111,7 @@ class DropDownButtonWidget extends StatelessWidget { color: AppColor.colorInputBorderCreateMailbox, width: 1, ), - color: colorButton ?? AppColor.colorInputBackgroundCreateMailbox, + color: colorButton, ), padding: const EdgeInsets.only(left: 12, right: 10), child: Row(children: [ @@ -125,39 +124,46 @@ class DropDownButtonWidget extends StatelessWidget { softWrap: CommonTextStyle.defaultSoftWrap, overflow: CommonTextStyle.defaultTextOverFlow, )), - iconArrowDown ?? SvgPicture.asset(_imagePaths.icDropDown) + iconArrowDown ?? SvgPicture.asset(imagePaths.icDropDown) ]), ), ) : null, onChanged: onChanged, - icon: iconArrowDown ?? SvgPicture.asset(_imagePaths.icDropDown), - buttonPadding: const EdgeInsets.symmetric(horizontal: 12), - buttonDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(radiusButton), - border: Border.all( - color: AppColor.colorInputBorderCreateMailbox, - width: 1, + buttonStyleData: ButtonStyleData( + height: heightItem, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(radiusButton), + border: Border.all( + color: AppColor.colorInputBorderCreateMailbox, + width: 1, + ), + color: colorButton, + ) + ), + dropdownStyleData: DropdownStyleData( + maxHeight: dropdownMaxHeight ?? 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(radiusButton), + color: Colors.white, + ), + width: dropdownWidth, + elevation: 4, + offset: const Offset(0.0, -8.0), + scrollbarTheme: ScrollbarThemeData( + radius: const Radius.circular(40), + thickness: MaterialStateProperty.all(6), + thumbVisibility: MaterialStateProperty.all(true), ), - color: colorButton ?? AppColor.colorInputBackgroundCreateMailbox, ), - itemHeight: heightItem, - buttonHeight: heightItem, - selectedItemHighlightColor: supportSelectionIcon - ? Colors.white - : Colors.black12, - itemPadding: const EdgeInsets.symmetric(horizontal: 12), - dropdownMaxHeight: dropdownMaxHeight ?? 200, - dropdownDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(radiusButton), - color: Colors.white, + iconStyleData: IconStyleData(icon: iconArrowDown ?? SvgPicture.asset(imagePaths.icDropDown)), + menuItemStyleData: MenuItemStyleData( + height: heightItem, + padding: const EdgeInsets.symmetric(horizontal: 12), + overlayColor: MaterialStateProperty.resolveWith((Set states) => supportSelectionIcon ? Colors.white : Colors.black12) ), - offset: const Offset(0.0, -8.0), - dropdownElevation: 4, - scrollbarRadius: const Radius.circular(40), - scrollbarThickness: 6, onMenuStateChange: onMenuStateChange, - dropdownWidth: dropdownWidth, ), ), ); @@ -167,13 +173,10 @@ class DropDownButtonWidget extends StatelessWidget { if (item is Identity) { return item.name ?? ''; } - if (item is Locale) { - return item.getLanguageName(context); - } if (item is FontNameType) { - return item.fontFamily; + return item.title; } - if (item is enough_html_editor.SafeFont) { + if (item is rich_text_composer.SafeFont) { return item.name; } if (item is rule_condition.Field) { diff --git a/lib/features/base/widget/hyper_link_widget.dart b/lib/features/base/widget/hyper_link_widget.dart new file mode 100644 index 0000000000..a06d0ac5c5 --- /dev/null +++ b/lib/features/base/widget/hyper_link_widget.dart @@ -0,0 +1,26 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:tmail_ui_user/features/base/styles/hyper_link_widget_styles.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; + +class HyperLinkWidget extends StatelessWidget { + + final String urlString; + + const HyperLinkWidget({Key? key, required this.urlString}) : super(key: key); + + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan( + text: urlString, + style: const TextStyle( + color: HyperLinkWidgetStyles.textColor, + fontSize: HyperLinkWidgetStyles.textSize, + decoration: TextDecoration.underline + ), + recognizer: TapGestureRecognizer()..onTap = () => AppUtils.launchLink(urlString) + ) + ); + } +} diff --git a/lib/features/base/widget/link_browser_widget.dart b/lib/features/base/widget/link_browser_widget.dart new file mode 100644 index 0000000000..1b738a8d37 --- /dev/null +++ b/lib/features/base/widget/link_browser_widget.dart @@ -0,0 +1,23 @@ + +import 'package:flutter/widgets.dart'; +import 'package:url_launcher/link.dart'; + +class LinkBrowserWidget extends StatelessWidget { + + final Uri uri; + final Widget child; + + const LinkBrowserWidget({ + Key? key, + required this.uri, + required this.child + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Link( + uri: uri, + builder: (context, function) => child + ); + } +} \ No newline at end of file diff --git a/lib/features/base/widget/material_text_button.dart b/lib/features/base/widget/material_text_button.dart index 4c483564cb..7a7774fc68 100644 --- a/lib/features/base/widget/material_text_button.dart +++ b/lib/features/base/widget/material_text_button.dart @@ -3,17 +3,19 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:flutter/material.dart'; typedef OnTapMaterialTextButton = Function(); +typedef OnLongPressMaterialTextButton = Function(); class MaterialTextButton extends StatelessWidget { final String label; - final OnTapMaterialTextButton onTap; + final OnTapMaterialTextButton? onTap; + final OnLongPressMaterialTextButton? onLongPress; final double borderRadius; final Color? labelColor; final double labelSize; final FontWeight? labelWeight; final TextStyle? customStyle; - final EdgeInsets? padding; + final EdgeInsetsGeometry? padding; final TextOverflow? overflow; final bool? softWrap; @@ -21,6 +23,7 @@ class MaterialTextButton extends StatelessWidget { Key? key, required this.label, required this.onTap, + this.onLongPress, this.borderRadius = 12, this.labelColor, this.labelSize = 15, @@ -37,6 +40,7 @@ class MaterialTextButton extends StatelessWidget { color: Colors.transparent, child: InkWell( onTap: onTap, + onLongPress: onLongPress, customBorder: RoundedRectangleBorder(borderRadius: BorderRadius.circular(borderRadius)), child: Padding( padding: padding ?? const EdgeInsets.symmetric(horizontal: 8, vertical: 5), diff --git a/lib/features/base/widget/material_text_icon_button.dart b/lib/features/base/widget/material_text_icon_button.dart new file mode 100644 index 0000000000..d3690a9ec2 --- /dev/null +++ b/lib/features/base/widget/material_text_icon_button.dart @@ -0,0 +1,65 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +typedef OnTapMaterialTextIconButton = Function(); + +class MaterialTextIconButton extends StatelessWidget { + + final String label; + final String icon; + final OnTapMaterialTextIconButton onTap; + final double borderRadius; + final double elevation; + final double iconSize; + final Size? minimumSize; + final Color? labelColor; + final Color? iconColor; + final Color? backgroundColor; + final TextStyle? labelStyle; + + const MaterialTextIconButton({ + Key? key, + required this.label, + required this.icon, + required this.onTap, + this.borderRadius = 12, + this.elevation = 0, + this.iconSize = 24, + this.labelColor, + this.iconColor, + this.backgroundColor, + this.labelStyle, + this.minimumSize + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ElevatedButton.icon( + onPressed: onTap, + icon: SvgPicture.asset( + icon, + width: iconSize, + height: iconSize, + fit: BoxFit.fill, + colorFilter: (iconColor ?? AppColor.colorTextButton).asFilter(), + ), + label: Text( + label, + style: labelStyle ?? TextStyle( + fontSize: 16, + color: labelColor ?? AppColor.colorTextButton, + fontWeight: FontWeight.w500 + ), + ), + style: ElevatedButton.styleFrom( + foregroundColor: labelColor ?? AppColor.colorTextButton, + backgroundColor: backgroundColor ?? AppColor.colorCreateNewIdentityButton, + elevation: 0, + minimumSize: minimumSize, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(borderRadius)), + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/base/widget/popup_item_no_icon_widget.dart b/lib/features/base/widget/popup_item_no_icon_widget.dart new file mode 100644 index 0000000000..4fbea9c64f --- /dev/null +++ b/lib/features/base/widget/popup_item_no_icon_widget.dart @@ -0,0 +1,63 @@ + +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; + +class PopupItemNoIconWidget extends StatelessWidget { + + final String _nameAction; + final String? svgIconSelected; + final bool isSelected; + final double? maxWidth; + final TextStyle? styleName; + final EdgeInsets? padding; + final VoidCallback? onCallbackAction; + + const PopupItemNoIconWidget( + this._nameAction, + { + Key? key, + this.isSelected = false, + this.svgIconSelected, + this.maxWidth, + this.styleName, + this.padding, + this.onCallbackAction + } + ) : super(key: key); + + @override + Widget build(BuildContext context) { + return PointerInterceptor( + child: InkWell( + onTap: onCallbackAction, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: SizedBox( + width: maxWidth, + child: Row(children: [ + Expanded(child: Text( + _nameAction, + style: const TextStyle( + fontSize: 17, + color: Colors.black, + fontWeight: FontWeight.normal + ) + )), + if (isSelected && svgIconSelected != null) + ...[ + const SizedBox(width: 12), + SvgPicture.asset( + svgIconSelected!, + width: 24, + height: 24, + fit: BoxFit.fill + ), + ] + ]), + ), + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/base/widget/popup_item_widget.dart b/lib/features/base/widget/popup_item_widget.dart index c16d87dd75..169f51205f 100644 --- a/lib/features/base/widget/popup_item_widget.dart +++ b/lib/features/base/widget/popup_item_widget.dart @@ -1,7 +1,8 @@ - +import 'package:core/presentation/extensions/color_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/base/styles/popup_item_widget_style.dart'; class PopupItemWidget extends StatelessWidget { @@ -10,7 +11,9 @@ class PopupItemWidget extends StatelessWidget { final Color? colorIcon; final double? iconSize; final TextStyle? styleName; - final EdgeInsets? padding; + final bool? isSelected; + final String? selectedIcon; + final EdgeInsetsGeometry? padding; final VoidCallback? onCallbackAction; const PopupItemWidget( @@ -21,7 +24,9 @@ class PopupItemWidget extends StatelessWidget { this.colorIcon, this.iconSize, this.styleName, + this.isSelected, this.padding, + this.selectedIcon, this.onCallbackAction } ) : super(key: key); @@ -31,28 +36,34 @@ class PopupItemWidget extends StatelessWidget { return PointerInterceptor( child: InkWell( onTap: onCallbackAction, - child: Padding( - padding: padding ?? const EdgeInsets.symmetric(horizontal: 20, vertical: 16), - child: SizedBox( - child: Row(children: [ - SvgPicture.asset( - _iconAction, - width: iconSize ?? 20, - height: iconSize ?? 20, - fit: BoxFit.fill, - color: colorIcon - ), - const SizedBox(width: 12), - Expanded(child: Text( - _nameAction, - style: styleName ?? const TextStyle( - fontSize: 17, - fontWeight: FontWeight.normal, - color: Colors.black - ) - )), - ]) - ), + child: Container( + height: PopupItemWidgetStyle.height, + constraints: const BoxConstraints(minWidth: PopupItemWidgetStyle.minWidth), + padding: padding, + child: Row(children: [ + SvgPicture.asset( + _iconAction, + width: iconSize ?? PopupItemWidgetStyle.iconSize, + height: iconSize ?? PopupItemWidgetStyle.iconSize, + fit: BoxFit.fill, + colorFilter: colorIcon?.asFilter() + ), + const SizedBox(width: PopupItemWidgetStyle.space), + Expanded(child: Text( + _nameAction, + style: styleName ?? PopupItemWidgetStyle.labelTextStyle + )), + if (isSelected == true && selectedIcon != null) + Padding( + padding: PopupItemWidgetStyle.iconSelectedPadding, + child: SvgPicture.asset( + selectedIcon!, + width: PopupItemWidgetStyle.selectedIconSize, + height: PopupItemWidgetStyle.selectedIconSize, + fit: BoxFit.fill + ), + ) + ]), ) ), ); diff --git a/lib/features/base/widget/text_input_field_builder.dart b/lib/features/base/widget/text_input_field_builder.dart index eed04de4ac..00d5e5b3d8 100644 --- a/lib/features/base/widget/text_input_field_builder.dart +++ b/lib/features/base/widget/text_input_field_builder.dart @@ -1,9 +1,10 @@ import 'package:core/core.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/base/widget/text_input_decoration_builder.dart'; -typedef OnChangeInputAction = Function(String? value); +typedef OnChangeInputAction = Function(String value); class TextInputFieldBuilder extends StatelessWidget { @@ -46,24 +47,23 @@ class TextInputFieldBuilder extends StatelessWidget { color: AppColor.colorContentEmail)), const SizedBox(height: 8) ], - (TextFieldBuilder() - ..onChange((value) => onChangeInputAction?.call(value)) - ..textInputAction(TextInputAction.next) - ..addController(editingController ?? TextEditingController()) - ..addFocusNode(focusNode) - ..textStyle(const TextStyle(color: Colors.black, fontSize: 16)) - ..keyboardType(inputType ?? TextInputType.text) - ..minLines(minLines) - ..maxLines(maxLines) - ..textDecoration((TextInputDecorationBuilder() - ..setContentPadding(const EdgeInsets.symmetric( - vertical: BuildUtils.isWeb ? 16 : 12, - horizontal: 12)) - ..setHintText(hint) - ..setFillColor(backgroundColor) - ..setErrorText(error)) - .build())) - .build() + TextFieldBuilder( + onTextChange: onChangeInputAction, + textInputAction: TextInputAction.next, + controller: editingController, + focusNode: focusNode, + textStyle: const TextStyle(color: Colors.black, fontSize: 16), + keyboardType: inputType ?? TextInputType.text, + textDirection: DirectionUtils.getDirectionByLanguage(context), + minLines: minLines, + maxLines: maxLines, + decoration: (TextInputDecorationBuilder() + ..setContentPadding(const EdgeInsets.symmetric(vertical: PlatformInfo.isWeb ? 16 : 12, horizontal: 12)) + ..setHintText(hint) + ..setFillColor(backgroundColor) + ..setErrorText(error)) + .build(), + ) ]); } } \ No newline at end of file diff --git a/lib/features/caching/caching_manager.dart b/lib/features/caching/caching_manager.dart index 117b46ab9f..9f9350809e 100644 --- a/lib/features/caching/caching_manager.dart +++ b/lib/features/caching/caching_manager.dart @@ -1,14 +1,24 @@ -import 'package:flutter/foundation.dart'; -import 'package:tmail_ui_user/features/caching/account_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/email_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/mailbox_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/recent_search_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/subscription_cache_client.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/file_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/caching/clients/account_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/new_email_hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/opened_email_hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/session_hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; +import 'package:tmail_ui_user/features/caching/clients/email_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/fcm_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/hive_cache_version_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/mailbox_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_search_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/subscription_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart'; -import 'package:tmail_ui_user/features/push_notification/data/local/fcm_cache_manager.dart'; - -import 'config/hive_cache_config.dart'; +import 'package:tmail_ui_user/features/offline_mode/controller/work_manager_controller.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart'; class CachingManager { final MailboxCacheClient _mailboxCacheClient; @@ -16,8 +26,14 @@ class CachingManager { final EmailCacheClient _emailCacheClient; final RecentSearchCacheClient _recentSearchCacheClient; final AccountCacheClient _accountCacheClient; - final FCMCacheManager _fcmCacheManager; + final FcmCacheClient _fcmCacheClient; final FCMSubscriptionCacheClient _fcmSubscriptionCacheClient; + final HiveCacheVersionClient _hiveCacheVersionClient; + final NewEmailHiveCacheClient _newEmailHiveCacheClient; + final OpenedEmailHiveCacheClient _openedEmailHiveCacheClient; + final FileUtils _fileUtils; + final SendingEmailCacheManager _sendingEmailCacheManager; + final SessionHiveCacheClient _sessionHiveCacheClient; CachingManager( this._mailboxCacheClient, @@ -25,8 +41,14 @@ class CachingManager { this._emailCacheClient, this._recentSearchCacheClient, this._accountCacheClient, - this._fcmCacheManager, + this._fcmCacheClient, this._fcmSubscriptionCacheClient, + this._hiveCacheVersionClient, + this._newEmailHiveCacheClient, + this._openedEmailHiveCacheClient, + this._fileUtils, + this._sendingEmailCacheManager, + this._sessionHiveCacheClient, ); Future clearAll() async { @@ -34,28 +56,86 @@ class CachingManager { _stateCacheClient.clearAllData(), _mailboxCacheClient.clearAllData(), _emailCacheClient.clearAllData(), + _fcmCacheClient.clearAllData(), + _fcmSubscriptionCacheClient.clearAllData(), _recentSearchCacheClient.clearAllData(), _accountCacheClient.clearAllData(), + if (PlatformInfo.isMobile) + ...[ + _sessionHiveCacheClient.clearAllData(), + _newEmailHiveCacheClient.clearAllData(), + _openedEmailHiveCacheClient.clearAllData(), + _clearSendingEmailCache(), + ] + ], eagerError: true); + } + + Future clearData() async { + await Future.wait([ + _stateCacheClient.clearAllData(), + _mailboxCacheClient.clearAllData(), + _emailCacheClient.clearAllData(), + _fcmCacheClient.clearAllData(), _fcmSubscriptionCacheClient.clearAllData(), - _fcmCacheManager.clearAllStateToRefresh() - ]); - } - - Future cleanEmailCache() async { - if (kIsWeb) { - await Future.wait([ - _stateCacheClient.deleteItem(StateType.email.value), - _emailCacheClient.clearAllData(), - ]); - } else { - await Future.wait([ - _stateCacheClient.deleteItem(StateType.email.value), - _emailCacheClient.deleteBox(), - ]); - } + _recentSearchCacheClient.clearAllData(), + if (PlatformInfo.isMobile) + ...[ + _newEmailHiveCacheClient.clearAllData(), + _openedEmailHiveCacheClient.clearAllData(), + _clearSendingEmailCache(), + ] + ], eagerError: true); + } + + Future clearEmailCacheAndStateCacheByTupleKey(AccountId accountId, Session session) { + return Future.wait([ + _stateCacheClient.deleteItem(StateType.email.getTupleKeyStored(accountId, session.username)), + _emailCacheClient.clearAllData(), + ], eagerError: true); + } + + Future clearEmailCacheAndAllStateCache() { + return Future.wait([ + _stateCacheClient.clearAllData(), + _emailCacheClient.clearAllData(), + ], eagerError: true); + } + + Future onUpgradeCache(int oldVersion, int newVersion) async { + log('CachingManager::onUpgradeCache():oldVersion $oldVersion | newVersion: $newVersion'); + await clearData(); + await storeCacheVersion(newVersion); + } + + Future storeCacheVersion(int newVersion) async { + log('CachingManager::storeCacheVersion()'); + return _hiveCacheVersionClient.storeVersion(newVersion); + } + + Future getLatestVersion() { + return _hiveCacheVersionClient.getLatestVersion(); } Future closeHive() async { return await HiveCacheConfig().closeHive(); } + + void clearAllFileInStorage() { + if (PlatformInfo.isMobile) { + _fileUtils.removeFolder(CachingConstants.newEmailsContentFolderName); + _fileUtils.removeFolder(CachingConstants.openedEmailContentFolderName); + } + } + + Future _clearSendingEmailCache() async { + final listSendingEmails = await _sendingEmailCacheManager.getAllSendingEmails(); + final sendingIds = listSendingEmails.map((sendingEmail) => sendingEmail.sendingId).toSet().toList(); + if (sendingIds.isNotEmpty) { + await Future.wait( + sendingIds.map(WorkManagerController().cancelByUniqueId), + eagerError: true + ); + await _sendingEmailCacheManager.clearAllSendingEmails(); + } + } } diff --git a/lib/features/caching/account_cache_client.dart b/lib/features/caching/clients/account_cache_client.dart similarity index 100% rename from lib/features/caching/account_cache_client.dart rename to lib/features/caching/clients/account_cache_client.dart diff --git a/lib/features/caching/authentication_info_cache_client.dart b/lib/features/caching/clients/authentication_info_cache_client.dart similarity index 100% rename from lib/features/caching/authentication_info_cache_client.dart rename to lib/features/caching/clients/authentication_info_cache_client.dart diff --git a/lib/features/caching/clients/cache_version_client.dart b/lib/features/caching/clients/cache_version_client.dart new file mode 100644 index 0000000000..d1501abcb3 --- /dev/null +++ b/lib/features/caching/clients/cache_version_client.dart @@ -0,0 +1,9 @@ + +abstract class CacheVersionClient { + + String get versionKey; + + Future storeVersion(int newVersion); + + Future getLatestVersion(); +} \ No newline at end of file diff --git a/lib/features/caching/clients/email_cache_client.dart b/lib/features/caching/clients/email_cache_client.dart new file mode 100644 index 0000000000..fc33ac0daf --- /dev/null +++ b/lib/features/caching/clients/email_cache_client.dart @@ -0,0 +1,9 @@ + +import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart'; +import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; + +class EmailCacheClient extends HiveCacheClient { + + @override + String get tableName => 'EmailCache'; +} \ No newline at end of file diff --git a/lib/features/caching/encryption_key_cache_client.dart b/lib/features/caching/clients/encryption_key_cache_client.dart similarity index 100% rename from lib/features/caching/encryption_key_cache_client.dart rename to lib/features/caching/clients/encryption_key_cache_client.dart diff --git a/lib/features/caching/clients/fcm_cache_client.dart b/lib/features/caching/clients/fcm_cache_client.dart new file mode 100644 index 0000000000..4f285e36f3 --- /dev/null +++ b/lib/features/caching/clients/fcm_cache_client.dart @@ -0,0 +1,9 @@ + +import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; + +class FcmCacheClient extends HiveCacheClient { + + @override + String get tableName => CachingConstants.fcmCacheBoxName; +} \ No newline at end of file diff --git a/lib/features/caching/clients/hive_cache_version_client.dart b/lib/features/caching/clients/hive_cache_version_client.dart new file mode 100644 index 0000000000..1dae434f87 --- /dev/null +++ b/lib/features/caching/clients/hive_cache_version_client.dart @@ -0,0 +1,29 @@ +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tmail_ui_user/features/caching/clients/cache_version_client.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; + +class HiveCacheVersionClient extends CacheVersionClient { + + final SharedPreferences _sharedPreferences; + final ExceptionThrower _exceptionThrower; + + HiveCacheVersionClient(this._sharedPreferences, this._exceptionThrower); + + @override + String get versionKey => 'HiveCacheVersion'; + + @override + Future getLatestVersion() { + return Future.sync(() { + final latestVersion = _sharedPreferences.getInt(versionKey); + return latestVersion; + }).catchError(_exceptionThrower.throwException); + } + + @override + Future storeVersion(int newVersion) { + return Future.sync(() async { + return await _sharedPreferences.setInt(versionKey, newVersion); + }).catchError(_exceptionThrower.throwException); + } +} \ No newline at end of file diff --git a/lib/features/caching/mailbox_cache_client.dart b/lib/features/caching/clients/mailbox_cache_client.dart similarity index 100% rename from lib/features/caching/mailbox_cache_client.dart rename to lib/features/caching/clients/mailbox_cache_client.dart diff --git a/lib/features/caching/clients/new_email_hive_cache_client.dart b/lib/features/caching/clients/new_email_hive_cache_client.dart new file mode 100644 index 0000000000..95f1833344 --- /dev/null +++ b/lib/features/caching/clients/new_email_hive_cache_client.dart @@ -0,0 +1,10 @@ + +import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/detailed_email_hive_cache.dart'; + +class NewEmailHiveCacheClient extends HiveCacheClient { + + @override + String get tableName => CachingConstants.newEmailCacheBoxName; +} \ No newline at end of file diff --git a/lib/features/caching/clients/opened_email_hive_cache_client.dart b/lib/features/caching/clients/opened_email_hive_cache_client.dart new file mode 100644 index 0000000000..b63d246927 --- /dev/null +++ b/lib/features/caching/clients/opened_email_hive_cache_client.dart @@ -0,0 +1,9 @@ +import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/detailed_email_hive_cache.dart'; + +class OpenedEmailHiveCacheClient extends HiveCacheClient { + + @override + String get tableName => CachingConstants.openedEmailCacheBoxName; +} \ No newline at end of file diff --git a/lib/features/caching/recent_login_url_cache_client.dart b/lib/features/caching/clients/recent_login_url_cache_client.dart similarity index 100% rename from lib/features/caching/recent_login_url_cache_client.dart rename to lib/features/caching/clients/recent_login_url_cache_client.dart diff --git a/lib/features/caching/recent_login_username_cache_client.dart b/lib/features/caching/clients/recent_login_username_cache_client.dart similarity index 100% rename from lib/features/caching/recent_login_username_cache_client.dart rename to lib/features/caching/clients/recent_login_username_cache_client.dart diff --git a/lib/features/caching/recent_search_cache_client.dart b/lib/features/caching/clients/recent_search_cache_client.dart similarity index 100% rename from lib/features/caching/recent_search_cache_client.dart rename to lib/features/caching/clients/recent_search_cache_client.dart diff --git a/lib/features/caching/clients/sending_email_hive_cache_client.dart b/lib/features/caching/clients/sending_email_hive_cache_client.dart new file mode 100644 index 0000000000..d3309c73e5 --- /dev/null +++ b/lib/features/caching/clients/sending_email_hive_cache_client.dart @@ -0,0 +1,13 @@ + +import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_email_hive_cache.dart'; + +class SendingEmailHiveCacheClient extends HiveCacheClient { + + @override + String get tableName => CachingConstants.sendingEmailCacheBoxName; + + @override + bool get encryption => true; +} \ No newline at end of file diff --git a/lib/features/caching/clients/session_hive_cache_client.dart b/lib/features/caching/clients/session_hive_cache_client.dart new file mode 100644 index 0000000000..9071d5cda9 --- /dev/null +++ b/lib/features/caching/clients/session_hive_cache_client.dart @@ -0,0 +1,13 @@ + +import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; +import 'package:tmail_ui_user/features/session/data/model/session_hive_obj.dart'; + +class SessionHiveCacheClient extends HiveCacheClient { + + @override + String get tableName => CachingConstants.sessionCacheBoxName; + + @override + bool get encryption => true; +} \ No newline at end of file diff --git a/lib/features/caching/state_cache_client.dart b/lib/features/caching/clients/state_cache_client.dart similarity index 100% rename from lib/features/caching/state_cache_client.dart rename to lib/features/caching/clients/state_cache_client.dart diff --git a/lib/features/caching/subscription_cache_client.dart b/lib/features/caching/clients/subscription_cache_client.dart similarity index 100% rename from lib/features/caching/subscription_cache_client.dart rename to lib/features/caching/clients/subscription_cache_client.dart diff --git a/lib/features/caching/token_oidc_cache_client.dart b/lib/features/caching/clients/token_oidc_cache_client.dart similarity index 100% rename from lib/features/caching/token_oidc_cache_client.dart rename to lib/features/caching/clients/token_oidc_cache_client.dart diff --git a/lib/features/caching/config/cache_version.dart b/lib/features/caching/config/cache_version.dart new file mode 100644 index 0000000000..a79acd0b2e --- /dev/null +++ b/lib/features/caching/config/cache_version.dart @@ -0,0 +1,4 @@ + +class CacheVersion { + static const int hiveDBVersion = 6; +} \ No newline at end of file diff --git a/lib/features/caching/config/hive_cache_client.dart b/lib/features/caching/config/hive_cache_client.dart index 493d7fad2d..89ca70241b 100644 --- a/lib/features/caching/config/hive_cache_client.dart +++ b/lib/features/caching/config/hive_cache_client.dart @@ -1,8 +1,10 @@ import 'dart:typed_data'; +import 'package:core/presentation/extensions/map_extensions.dart'; import 'package:hive/hive.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; abstract class HiveCacheClient { @@ -55,8 +57,11 @@ abstract class HiveCacheClient { }); } - Future getItem(String key) { + Future getItem(String key, {bool needToReopen = false}) { return Future.sync(() async { + if (needToReopen) { + await closeBox(); + } final boxItem = encryption ? await openBoxEncryption() : await openBox(); @@ -68,20 +73,46 @@ abstract class HiveCacheClient { Future> getAll() { return Future.sync(() async { - final boxItem = encryption - ? await openBoxEncryption() - : await openBox(); + final boxItem = encryption ? await openBoxEncryption() : await openBox(); return boxItem.values.toList(); }).catchError((error) { throw error; }); } + Future> getListByTupleKey(String accountId, String userName) { + return Future.sync(() async { + final boxItem = encryption ? await openBoxEncryption() : await openBox(); + return boxItem.toMap() + .where((key, value) => _matchedKey(key, accountId, userName)) + .values + .toList(); + }).catchError((error) { + throw error; + }); + } + + Future> getValuesByListKey(List listKeys) { + return Future.sync(() async { + final boxItem = encryption ? await openBoxEncryption() : await openBox(); + return boxItem.toMap() + .where((key, value) => listKeys.contains(key)) + .values + .toList(); + }).catchError((error) { + throw error; + }); + } + + bool _matchedKey(String key, String accountId, String userName) { + final keyDecoded = CacheUtils.decodeKey(key); + final tupleKey = TupleKey.fromString(keyDecoded); + return tupleKey.parts.length >= 3 && tupleKey.parts[1] == accountId && tupleKey.parts[2] == userName; + } + Future updateItem(String key, T newObject) { return Future.sync(() async { - final boxItem = encryption - ? await openBoxEncryption() - : await openBox(); + final boxItem = encryption ? await openBoxEncryption() : await openBox(); return boxItem.put(key, newObject); }).catchError((error) { throw error; @@ -90,9 +121,7 @@ abstract class HiveCacheClient { Future updateMultipleItem(Map mapObject) { return Future.sync(() async { - final boxItem = encryption - ? await openBoxEncryption() - : await openBox(); + final boxItem = encryption ? await openBoxEncryption() : await openBox(); return boxItem.putAll(mapObject); }).catchError((error) { throw error; @@ -101,9 +130,7 @@ abstract class HiveCacheClient { Future deleteItem(String key) { return Future.sync(() async { - final boxItem = encryption - ? await openBoxEncryption() - : await openBox(); + final boxItem = encryption ? await openBoxEncryption() : await openBox(); return boxItem.delete(key); }).catchError((error) { throw error; @@ -154,4 +181,11 @@ abstract class HiveCacheClient { throw error; }); } + + Future closeBox() async { + if (Hive.isBoxOpen(tableName)) { + await Hive.box(tableName).close(); + } + return Future.value(); + } } \ No newline at end of file diff --git a/lib/features/caching/config/hive_cache_config.dart b/lib/features/caching/config/hive_cache_config.dart index fdcd511232..fb54ece116 100644 --- a/lib/features/caching/config/hive_cache_config.dart +++ b/lib/features/caching/config/hive_cache_config.dart @@ -3,9 +3,12 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:tmail_ui_user/features/caching/caching_manager.dart'; +import 'package:tmail_ui_user/features/caching/config/cache_version.dart'; import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; import 'package:tmail_ui_user/features/login/data/local/encryption_key_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/model/account_cache.dart'; @@ -19,7 +22,12 @@ import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_rights_cache.d import 'package:tmail_ui_user/features/mailbox/data/model/state_cache.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/recent_search_cache.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/attachment_hive_cache.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/detailed_email_hive_cache.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/email_header_hive_cache.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_email_hive_cache.dart'; import 'package:tmail_ui_user/features/push_notification/data/model/fcm_subscription.dart'; +import 'package:tmail_ui_user/features/session/data/model/session_hive_obj.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_address_hive_cache.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; @@ -34,13 +42,22 @@ class HiveCacheConfig { if (databasePath != null) { Hive.init(databasePath); } else { - if (!GetPlatform.isWeb) { + if (PlatformInfo.isMobile) { Directory directory = await path_provider.getApplicationDocumentsDirectory(); Hive.init(directory.path); } } } + Future onUpgradeDatabase(CachingManager cachingManager) async { + final oldVersion = await cachingManager.getLatestVersion() ?? 0; + const newVersion = CacheVersion.hiveDBVersion; + log('HiveCacheConfig::onUpgradeDatabase():oldVersion: $oldVersion | newVersion: $newVersion'); + if (oldVersion != newVersion) { + await cachingManager.onUpgradeCache(oldVersion, newVersion); + } + } + static Future initializeEncryptionKey() async { final encryptionKeyCacheManager = Get.find(); final encryptionKeyCache = await encryptionKeyCacheManager.getEncryptionKeyStored(); @@ -119,9 +136,29 @@ class HiveCacheConfig { RecentLoginUsernameCacheAdapter(), CachingConstants.RECENT_LOGIN_USERNAME_HIVE_CACHE_IDENTITY ); - registerCacheAdapter( + registerCacheAdapter( FCMSubscriptionCacheAdapter(), - CachingConstants.FCM_SUBSCRIPTION_HIVE_CACHE_INDENTITY + CachingConstants.FCM_SUBSCRIPTION_HIVE_CACHE_IDENTITY + ); + registerCacheAdapter( + AttachmentHiveCacheAdapter(), + CachingConstants.ATTACHMENT_HIVE_CACHE_ID + ); + registerCacheAdapter( + EmailHeaderHiveCacheAdapter(), + CachingConstants.EMAIL_HEADER_HIVE_CACHE_ID + ); + registerCacheAdapter( + DetailedEmailHiveCacheAdapter(), + CachingConstants.DETAILED_EMAIL_HIVE_CACHE_ID + ); + registerCacheAdapter( + SendingEmailHiveCacheAdapter(), + CachingConstants.SENDING_EMAIL_HIVE_CACHE_ID + ); + registerCacheAdapter( + SessionHiveObjAdapter(), + CachingConstants.typeIdSessionHiveObj ); } diff --git a/lib/features/caching/email_cache_client.dart b/lib/features/caching/email_cache_client.dart deleted file mode 100644 index 40df7e3ff9..0000000000 --- a/lib/features/caching/email_cache_client.dart +++ /dev/null @@ -1,23 +0,0 @@ - -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:tmail_ui_user/features/caching/config/hive_cache_client.dart'; -import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; - -class EmailCacheClient extends HiveCacheClient { - - @override - String get tableName => 'EmailCache'; - - Future> getListEmailCacheByMailboxId(MailboxId mailboxId) { - return Future.sync(() async { - final boxEmail = await openBox(); - return boxEmail.values.where((emailCache) { - return emailCache.mailboxIds != null - && emailCache.mailboxIds!.containsKey(mailboxId.id.value) - && emailCache.mailboxIds![mailboxId.id.value] == true; - }).toList(); - }).catchError((error) { - throw error; - }); - } -} \ No newline at end of file diff --git a/lib/features/caching/utils/cache_utils.dart b/lib/features/caching/utils/cache_utils.dart new file mode 100644 index 0000000000..954395754a --- /dev/null +++ b/lib/features/caching/utils/cache_utils.dart @@ -0,0 +1,41 @@ + +import 'dart:convert'; + +class CacheUtils { + static String encodeKey(String key) => base64.encode(utf8.encode(key)); + + static String decodeKey(String keyEncoded) => utf8.decode(base64.decode(keyEncoded)); +} + +class TupleKey { + final List parts; + + TupleKey( + String key1, + [ + String? key2, + String? key3, + String? key4, + ] + ) : parts = [ + key1, + if (key2 != null) key2, + if (key3 != null) key3, + if (key4 != null) key4, + ]; + + const TupleKey.byParts(this.parts); + + TupleKey.fromString(String multiKeyString) : parts = multiKeyString.split('|').toList(); + + @override + String toString() => parts.join('|'); + + @override + bool operator ==(other) => parts.toString() == other.toString(); + + @override + int get hashCode => Object.hashAll(parts); + + String get encodeKey => CacheUtils.encodeKey(toString()); +} \ No newline at end of file diff --git a/lib/features/caching/utils/caching_constants.dart b/lib/features/caching/utils/caching_constants.dart index 9235ddcc61..69c46a5828 100644 --- a/lib/features/caching/utils/caching_constants.dart +++ b/lib/features/caching/utils/caching_constants.dart @@ -13,5 +13,22 @@ class CachingConstants { static const int AUTHENTICATION_INFO_HIVE_CACHE_IDENTIFY = 11; static const int RECENT_LOGIN_URL_HIVE_CACHE_IDENTITY = 12; static const int RECENT_LOGIN_USERNAME_HIVE_CACHE_IDENTITY = 13; - static const int FCM_SUBSCRIPTION_HIVE_CACHE_INDENTITY = 14; + static const int FCM_SUBSCRIPTION_HIVE_CACHE_IDENTITY = 14; + static const int ATTACHMENT_HIVE_CACHE_ID = 15; + static const int EMAIL_HEADER_HIVE_CACHE_ID = 16; + static const int DETAILED_EMAIL_HIVE_CACHE_ID = 17; + static const int SENDING_EMAIL_HIVE_CACHE_ID = 18; + static const int typeIdSessionHiveObj = 19; + + static const String fcmCacheBoxName = 'fcm_cache_box'; + static const String newEmailCacheBoxName = 'new_email_cache_box'; + static const String openedEmailCacheBoxName = 'opened_email_cache_box'; + static const String sendingEmailCacheBoxName = 'sending_email_cache_box'; + static const String sessionCacheBoxName = 'session_cache_box'; + + static const String newEmailsContentFolderName = 'new_emails'; + static const String openedEmailContentFolderName = 'opened_email'; + + static const int maxNumberNewEmailsForOffline = 10; + static const int maxNumberOpenedEmailsForOffline = 30; } \ No newline at end of file diff --git a/lib/features/cleanup/data/datasource_impl/cleanup_datasource_impl.dart b/lib/features/cleanup/data/datasource_impl/cleanup_datasource_impl.dart index 19a6ebcfcd..73117d5f05 100644 --- a/lib/features/cleanup/data/datasource_impl/cleanup_datasource_impl.dart +++ b/lib/features/cleanup/data/datasource_impl/cleanup_datasource_impl.dart @@ -30,35 +30,27 @@ class CleanupDataSourceImpl extends CleanupDataSource { Future cleanEmailCache(EmailCleanupRule cleanupRule) { return Future.sync(() async { return await emailCacheManager.clean(cleanupRule); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future cleanRecentSearchCache(RecentSearchCleanupRule cleanupRule) { return Future.sync(() async { return await recentSearchCacheManager.clean(cleanupRule); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future cleanRecentLoginUrlCache(RecentLoginUrlCleanupRule cleanupRule) { return Future.sync(() async { return await recentLoginUrlCacheManager.clean(cleanupRule); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future cleanRecentLoginUsernameCache(RecentLoginUsernameCleanupRule cleanupRule) { return Future.sync(() async { return await recentLoginUsernameCacheManager.clean(cleanupRule); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/cleanup/data/local/recent_login_url_cache_manager.dart b/lib/features/cleanup/data/local/recent_login_url_cache_manager.dart index 8b32cecb29..2574e961a4 100644 --- a/lib/features/cleanup/data/local/recent_login_url_cache_manager.dart +++ b/lib/features/cleanup/data/local/recent_login_url_cache_manager.dart @@ -1,5 +1,5 @@ -import 'package:tmail_ui_user/features/caching/recent_login_url_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_login_url_cache_client.dart'; import 'package:tmail_ui_user/features/cleanup/domain/model/recent_login_url_cleanup_rule.dart'; import 'package:tmail_ui_user/features/login/data/model/recent_login_url_cache.dart'; diff --git a/lib/features/cleanup/data/local/recent_login_username_cache_manager.dart b/lib/features/cleanup/data/local/recent_login_username_cache_manager.dart index 25c75e3f67..cb7093f8b5 100644 --- a/lib/features/cleanup/data/local/recent_login_username_cache_manager.dart +++ b/lib/features/cleanup/data/local/recent_login_username_cache_manager.dart @@ -1,5 +1,5 @@ -import 'package:tmail_ui_user/features/caching/recent_login_username_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_login_username_cache_client.dart'; import 'package:tmail_ui_user/features/cleanup/domain/model/recent_login_username_cleanup_rule.dart'; import 'package:tmail_ui_user/features/login/data/model/recent_login_username_cache.dart'; diff --git a/lib/features/cleanup/data/local/recent_search_cache_manager.dart b/lib/features/cleanup/data/local/recent_search_cache_manager.dart index 2a9e38d6d6..c72e1cca0c 100644 --- a/lib/features/cleanup/data/local/recent_search_cache_manager.dart +++ b/lib/features/cleanup/data/local/recent_search_cache_manager.dart @@ -1,5 +1,5 @@ -import 'package:tmail_ui_user/features/caching/recent_search_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_search_cache_client.dart'; import 'package:tmail_ui_user/features/cleanup/domain/model/recent_search_cleanup_rule.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/recent_search_cache.dart'; diff --git a/lib/features/cleanup/domain/state/cleanup_email_cache_state.dart b/lib/features/cleanup/domain/state/cleanup_email_cache_state.dart index 3b26aa87e4..553719ac3e 100644 --- a/lib/features/cleanup/domain/state/cleanup_email_cache_state.dart +++ b/lib/features/cleanup/domain/state/cleanup_email_cache_state.dart @@ -1,18 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; -class CleanupEmailCacheSuccess extends UIState { - - CleanupEmailCacheSuccess(); - - @override - List get props => []; -} +class CleanupEmailCacheSuccess extends UIState {} class CleanupEmailCacheFailure extends FeatureFailure { - final exception; - - CleanupEmailCacheFailure(this.exception); - @override - List get props => [exception]; + CleanupEmailCacheFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/cleanup/domain/state/cleanup_recent_login_url_cache_state.dart b/lib/features/cleanup/domain/state/cleanup_recent_login_url_cache_state.dart index 6d5c0105ca..434fe08c6e 100644 --- a/lib/features/cleanup/domain/state/cleanup_recent_login_url_cache_state.dart +++ b/lib/features/cleanup/domain/state/cleanup_recent_login_url_cache_state.dart @@ -1,18 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; -class CleanupRecentLoginUrlCacheSuccess extends UIState { - - CleanupRecentLoginUrlCacheSuccess(); - - @override - List get props => []; -} +class CleanupRecentLoginUrlCacheSuccess extends UIState {} class CleanupRecentLoginUrlCacheFailure extends FeatureFailure { - final dynamic exception; - - CleanupRecentLoginUrlCacheFailure(this.exception); - @override - List get props => [exception]; + CleanupRecentLoginUrlCacheFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/cleanup/domain/state/cleanup_recent_login_username_cache_state.dart b/lib/features/cleanup/domain/state/cleanup_recent_login_username_cache_state.dart index dcba4cb287..32d2bdd7c9 100644 --- a/lib/features/cleanup/domain/state/cleanup_recent_login_username_cache_state.dart +++ b/lib/features/cleanup/domain/state/cleanup_recent_login_username_cache_state.dart @@ -1,18 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; -class CleanupRecentLoginUsernameCacheSuccess extends UIState { - - CleanupRecentLoginUsernameCacheSuccess(); - - @override - List get props => []; -} +class CleanupRecentLoginUsernameCacheSuccess extends UIState {} class CleanupRecentLoginUsernameCacheFailure extends FeatureFailure { - final dynamic exception; - - CleanupRecentLoginUsernameCacheFailure(this.exception); - @override - List get props => [exception]; + CleanupRecentLoginUsernameCacheFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/cleanup/domain/state/cleanup_recent_search_cache_state.dart b/lib/features/cleanup/domain/state/cleanup_recent_search_cache_state.dart index 9a1dadbdbc..19ee7a2660 100644 --- a/lib/features/cleanup/domain/state/cleanup_recent_search_cache_state.dart +++ b/lib/features/cleanup/domain/state/cleanup_recent_search_cache_state.dart @@ -1,18 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; -class CleanupRecentSearchCacheSuccess extends UIState { - - CleanupRecentSearchCacheSuccess(); - - @override - List get props => []; -} +class CleanupRecentSearchCacheSuccess extends UIState {} class CleanupRecentSearchCacheFailure extends FeatureFailure { - final dynamic exception; - - CleanupRecentSearchCacheFailure(this.exception); - @override - List get props => [exception]; + CleanupRecentSearchCacheFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/composer/data/datasource_impl/composer_datasource_impl.dart b/lib/features/composer/data/datasource_impl/composer_datasource_impl.dart index b856415182..2575533daf 100644 --- a/lib/features/composer/data/datasource_impl/composer_datasource_impl.dart +++ b/lib/features/composer/data/datasource_impl/composer_datasource_impl.dart @@ -30,8 +30,6 @@ class ComposerDataSourceImpl extends ComposerDataSource { bytesData: fileInfo.bytes, maxWidth: maxWidth, compress: compress); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/composer/data/datasource_impl/contact_datasource_impl.dart b/lib/features/composer/data/datasource_impl/contact_datasource_impl.dart index 1034076c1a..be1945044b 100644 --- a/lib/features/composer/data/datasource_impl/contact_datasource_impl.dart +++ b/lib/features/composer/data/datasource_impl/contact_datasource_impl.dart @@ -25,9 +25,7 @@ class ContactDataSourceImpl extends ContactDataSource { return []; } } - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } List _toDeviceContact(contact_service.Contact contact) { diff --git a/lib/features/composer/domain/extensions/email_request_extension.dart b/lib/features/composer/domain/extensions/email_request_extension.dart new file mode 100644 index 0000000000..f2a202b74f --- /dev/null +++ b/lib/features/composer/domain/extensions/email_request_extension.dart @@ -0,0 +1,21 @@ + +import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +extension EmailRequestExtension on EmailRequest { + SendingEmail toSendingEmail(String sendingId, {CreateNewMailboxRequest? mailboxRequest}) { + return SendingEmail( + sendingId: sendingId, + email: email, + emailActionType: emailActionType, + createTime: DateTime.now(), + sentMailboxId: sentMailboxId, + emailIdDestroyed: emailIdDestroyed, + emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, + identityId: identityId, + mailboxNameRequest: mailboxRequest?.newName, + creationIdRequest: mailboxRequest?.creationId + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/domain/model/email_request.dart b/lib/features/composer/domain/model/email_request.dart index 9cfeac9ac4..e340b19aa6 100644 --- a/lib/features/composer/domain/model/email_request.dart +++ b/lib/features/composer/domain/model/email_request.dart @@ -1,43 +1,43 @@ import 'package:equatable/equatable.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/email_action_type.dart'; class EmailRequest with EquatableMixin { final Email email; - final Id submissionCreateId; final MailboxId? sentMailboxId; final EmailId? emailIdDestroyed; - final Identity? identity; + final EmailId? emailIdAnsweredOrForwarded; + final IdentityId? identityId; + final EmailActionType emailActionType; + final String? storedSendingId; - EmailRequest(this.email, this.submissionCreateId, { + EmailRequest({ + required this.email, + required this.emailActionType, this.sentMailboxId, - this.identity, - this.emailIdDestroyed + this.identityId, + this.emailIdDestroyed, + this.emailIdAnsweredOrForwarded, + this.storedSendingId, }); @override List get props => [ email, - submissionCreateId, sentMailboxId, - identity, - emailIdDestroyed + identityId, + emailIdDestroyed, + emailIdAnsweredOrForwarded, + emailActionType, + storedSendingId, ]; -} -extension EmailRequestExtension on EmailRequest { + bool get isEmailAnswered => emailIdAnsweredOrForwarded != null && + (emailActionType == EmailActionType.reply || emailActionType == EmailActionType.replyAll); - EmailRequest toEmailRequest({Email? newEmail}) { - return EmailRequest( - newEmail ?? email, - submissionCreateId, - sentMailboxId: sentMailboxId, - identity: identity, - emailIdDestroyed: emailIdDestroyed - ); - } + bool get isEmailForwarded => emailIdAnsweredOrForwarded != null && emailActionType == EmailActionType.forward; } \ No newline at end of file diff --git a/lib/features/composer/domain/state/download_image_as_base64_state.dart b/lib/features/composer/domain/state/download_image_as_base64_state.dart index fd0e5481de..af801f1a28 100644 --- a/lib/features/composer/domain/state/download_image_as_base64_state.dart +++ b/lib/features/composer/domain/state/download_image_as_base64_state.dart @@ -3,31 +3,34 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:model/upload/file_info.dart'; -class DownloadingImageAsBase64 extends UIState { - - DownloadingImageAsBase64(); - - @override - List get props => []; -} +class DownloadingImageAsBase64 extends UIState {} class DownloadImageAsBase64Success extends UIState { final String base64Uri; final String cid; final FileInfo fileInfo; + final bool fromFileShared; - DownloadImageAsBase64Success(this.base64Uri, this.cid, this.fileInfo); + DownloadImageAsBase64Success( + this.base64Uri, + this.cid, + this.fileInfo, + { + this.fromFileShared = false + } + ); @override - List get props => [base64Uri, cid, fileInfo]; + List get props => [ + base64Uri, + cid, + fileInfo, + fromFileShared, + ]; } class DownloadImageAsBase64Failure extends FeatureFailure { - final dynamic exception; - - DownloadImageAsBase64Failure(this.exception); - @override - List get props => [exception]; + DownloadImageAsBase64Failure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/composer/domain/state/get_autocomplete_state.dart b/lib/features/composer/domain/state/get_autocomplete_state.dart index f8b95c2c40..6a3535fad6 100644 --- a/lib/features/composer/domain/state/get_autocomplete_state.dart +++ b/lib/features/composer/domain/state/get_autocomplete_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; class GetAutoCompleteSuccess extends UIState { @@ -12,10 +13,6 @@ class GetAutoCompleteSuccess extends UIState { } class GetAutoCompleteFailure extends FeatureFailure { - final dynamic exception; - GetAutoCompleteFailure(this.exception); - - @override - List get props => [exception]; + GetAutoCompleteFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/composer/domain/state/get_device_contact_suggestions_state.dart b/lib/features/composer/domain/state/get_device_contact_suggestions_state.dart index ed645f11f5..cbda3c4f61 100644 --- a/lib/features/composer/domain/state/get_device_contact_suggestions_state.dart +++ b/lib/features/composer/domain/state/get_device_contact_suggestions_state.dart @@ -1,6 +1,6 @@ - -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/contact/contact.dart'; class GetDeviceContactSuggestionsSuccess extends UIState { final List results; @@ -12,10 +12,6 @@ class GetDeviceContactSuggestionsSuccess extends UIState { } class GetDeviceContactSuggestionsFailure extends FeatureFailure { - final dynamic exception; - - GetDeviceContactSuggestionsFailure(this.exception); - @override - List get props => [exception]; + GetDeviceContactSuggestionsFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/composer/domain/state/save_email_address_state.dart b/lib/features/composer/domain/state/save_email_address_state.dart index e8ac4550de..1695991f6c 100644 --- a/lib/features/composer/domain/state/save_email_address_state.dart +++ b/lib/features/composer/domain/state/save_email_address_state.dart @@ -1,18 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; -class SaveEmailAddressSuccess extends UIState { - - SaveEmailAddressSuccess(); - - @override - List get props => []; -} +class SaveEmailAddressSuccess extends UIState {} class SaveEmailAddressFailure extends FeatureFailure { - final dynamic exception; - - SaveEmailAddressFailure(this.exception); - @override - List get props => [exception]; + SaveEmailAddressFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/composer/domain/state/save_email_as_drafts_state.dart b/lib/features/composer/domain/state/save_email_as_drafts_state.dart index d9a985bd30..e92f9d7192 100644 --- a/lib/features/composer/domain/state/save_email_as_drafts_state.dart +++ b/lib/features/composer/domain/state/save_email_as_drafts_state.dart @@ -1,15 +1,10 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -class SaveEmailAsDraftsLoading extends UIState { - - SaveEmailAsDraftsLoading(); - - @override - List get props => []; -} +class SaveEmailAsDraftsLoading extends UIState {} class SaveEmailAsDraftsSuccess extends UIActionState { @@ -24,14 +19,10 @@ class SaveEmailAsDraftsSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => []; + List get props => [emailAsDrafts, ...super.props]; } class SaveEmailAsDraftsFailure extends FeatureFailure { - final dynamic exception; - - SaveEmailAsDraftsFailure(this.exception); - @override - List get props => [exception]; + SaveEmailAsDraftsFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/composer/domain/state/send_email_state.dart b/lib/features/composer/domain/state/send_email_state.dart index 5c866fe43b..9bfeecfba0 100644 --- a/lib/features/composer/domain/state/send_email_state.dart +++ b/lib/features/composer/domain/state/send_email_state.dart @@ -1,13 +1,14 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; -class SendingEmailState extends UIState { - SendingEmailState(); - - @override - List get props => []; -} +class SendEmailLoading extends UIState {} class SendEmailSuccess extends UIActionState { @@ -15,16 +16,22 @@ class SendEmailSuccess extends UIActionState { jmap.State? currentEmailState, jmap.State? currentMailboxState, }) : super(currentEmailState, currentMailboxState); - - @override - List get props => []; } class SendEmailFailure extends FeatureFailure { - final dynamic exception; - - SendEmailFailure(this.exception); - @override - List get props => [exception]; + final Session session; + final AccountId accountId; + final EmailRequest emailRequest; + final CreateNewMailboxRequest? mailboxRequest; + final SendingEmailActionType? sendingEmailActionType; + + SendEmailFailure({ + dynamic exception, + required this.session, + required this.accountId, + required this.emailRequest, + this.mailboxRequest, + this.sendingEmailActionType + }) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/composer/domain/state/update_email_drafts_state.dart b/lib/features/composer/domain/state/update_email_drafts_state.dart index 9729723a21..af7ae68776 100644 --- a/lib/features/composer/domain/state/update_email_drafts_state.dart +++ b/lib/features/composer/domain/state/update_email_drafts_state.dart @@ -1,15 +1,10 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -class UpdatingEmailDrafts extends UIState { - - UpdatingEmailDrafts(); - - @override - List get props => []; -} +class UpdatingEmailDrafts extends UIState {} class UpdateEmailDraftsSuccess extends UIActionState { @@ -24,14 +19,10 @@ class UpdateEmailDraftsSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => []; + List get props => [emailAsDrafts, ...super.props]; } class UpdateEmailDraftsFailure extends FeatureFailure { - final dynamic exception; - - UpdateEmailDraftsFailure(this.exception); - @override - List get props => [exception]; + UpdateEmailDraftsFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/composer/domain/state/upload_attachment_state.dart b/lib/features/composer/domain/state/upload_attachment_state.dart index 87c169d055..c58d04d11e 100644 --- a/lib/features/composer/domain/state/upload_attachment_state.dart +++ b/lib/features/composer/domain/state/upload_attachment_state.dart @@ -7,19 +7,40 @@ class UploadAttachmentSuccess extends UIState { final UploadAttachment uploadAttachment; final bool isInline; + final bool fromFileShared; - UploadAttachmentSuccess(this.uploadAttachment, {this.isInline = false}); + UploadAttachmentSuccess( + this.uploadAttachment, + { + this.isInline = false, + this.fromFileShared = false, + } + ); @override - List get props => [uploadAttachment, isInline]; + List get props => [ + uploadAttachment, + isInline, + fromFileShared, + ]; } class UploadAttachmentFailure extends FeatureFailure { - final dynamic exception; final bool isInline; + final bool fromFileShared; - UploadAttachmentFailure(this.exception, {this.isInline = false}); + UploadAttachmentFailure( + dynamic exception, + { + this.isInline = false, + this.fromFileShared = false, + } + ) : super(exception: exception); @override - List get props => [exception, isInline]; + List get props => [ + isInline, + fromFileShared, + ...super.props + ]; } \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart b/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart index 3f37fb5bf7..b3fb5d0ac5 100644 --- a/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart +++ b/lib/features/composer/domain/usecases/download_image_as_base64_interactor.dart @@ -10,24 +10,31 @@ class DownloadImageAsBase64Interactor { DownloadImageAsBase64Interactor(this._composerRepository); Stream> execute( - String url, - String cid, - FileInfo fileInfo, - { - double? maxWidth, - bool? compress - } + String url, + String cid, + FileInfo fileInfo, + { + double? maxWidth, + bool? compress, + bool fromFileShared = false, + } ) async* { try { yield Right(DownloadingImageAsBase64()); final result = await _composerRepository.downloadImageAsBase64( - url, + url, + cid, + fileInfo, + maxWidth: maxWidth, + compress: compress + ); + if (result?.isNotEmpty == true) { + yield Right(DownloadImageAsBase64Success( + result!, cid, fileInfo, - maxWidth: maxWidth, - compress: compress); - if (result?.isNotEmpty == true) { - yield Right(DownloadImageAsBase64Success(result!, cid, fileInfo)); + fromFileShared: fromFileShared + )); } else { yield Left(DownloadImageAsBase64Failure(null)); } diff --git a/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart b/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart index 2141a70eea..511f3d181c 100644 --- a/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart +++ b/lib/features/composer/domain/usecases/save_email_as_drafts_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; @@ -12,19 +13,19 @@ class SaveEmailAsDraftsInteractor { SaveEmailAsDraftsInteractor(this._emailRepository, this._mailboxRepository); - Stream> execute(AccountId accountId, Email email) async* { + Stream> execute(Session session, AccountId accountId, Email email) async* { try { yield Right(SaveEmailAsDraftsLoading()); final listState = await Future.wait([ - _mailboxRepository.getMailboxState(), - _emailRepository.getEmailState(), + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), ], eagerError: true); final currentMailboxState = listState.first; final currentEmailState = listState.last; - final emailAsDrafts = await _emailRepository.saveEmailAsDrafts(accountId, email); + final emailAsDrafts = await _emailRepository.saveEmailAsDrafts(session, accountId, email); yield Right( SaveEmailAsDraftsSuccess( emailAsDrafts, diff --git a/lib/features/composer/domain/usecases/send_email_interactor.dart b/lib/features/composer/domain/usecases/send_email_interactor.dart index 7eb7f45913..dda7f13d5a 100644 --- a/lib/features/composer/domain/usecases/send_email_interactor.dart +++ b/lib/features/composer/domain/usecases/send_email_interactor.dart @@ -2,11 +2,13 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; class SendEmailInteractor { final EmailRepository _emailRepository; @@ -15,28 +17,37 @@ class SendEmailInteractor { SendEmailInteractor(this._emailRepository, this._mailboxRepository); Stream> execute( - AccountId accountId, - EmailRequest emailRequest, - {CreateNewMailboxRequest? mailboxRequest} + Session session, + AccountId accountId, + EmailRequest emailRequest, + { + CreateNewMailboxRequest? mailboxRequest, + SendingEmailActionType? sendingEmailActionType + } ) async* { try { - yield Right(SendingEmailState()); + yield Right(SendEmailLoading()); final listState = await Future.wait([ - _mailboxRepository.getMailboxState(), - _emailRepository.getEmailState(), + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), ], eagerError: true); final currentMailboxState = listState.first; final currentEmailState = listState.last; final result = await _emailRepository.sendEmail( + session, accountId, emailRequest, mailboxRequest: mailboxRequest ); if (result) { + if (emailRequest.emailIdDestroyed != null) { + await _emailRepository.deleteEmailPermanently(session, accountId, emailRequest.emailIdDestroyed!); + } + yield Right( SendEmailSuccess( currentEmailState: currentEmailState, @@ -44,10 +55,23 @@ class SendEmailInteractor { ) ); } else { - yield Left(SendEmailFailure(null)); + yield Left(SendEmailFailure( + session: session, + accountId: accountId, + emailRequest: emailRequest, + mailboxRequest: mailboxRequest, + sendingEmailActionType: sendingEmailActionType, + )); } } catch (e) { - yield Left(SendEmailFailure(e)); + yield Left(SendEmailFailure( + exception: e, + session: session, + accountId: accountId, + emailRequest: emailRequest, + mailboxRequest: mailboxRequest, + sendingEmailActionType: sendingEmailActionType, + )); } } } \ No newline at end of file diff --git a/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart b/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart index 93a4f2acbb..77c74ae594 100644 --- a/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart +++ b/lib/features/composer/domain/usecases/update_email_drafts_interactor.dart @@ -1,6 +1,8 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; @@ -12,19 +14,19 @@ class UpdateEmailDraftsInteractor { UpdateEmailDraftsInteractor(this._emailRepository, this._mailboxRepository); - Stream> execute(AccountId accountId, Email newEmail, EmailId oldEmailId) async* { + Stream> execute(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId) async* { try { yield Right(UpdatingEmailDrafts()); final listState = await Future.wait([ - _mailboxRepository.getMailboxState(), - _emailRepository.getEmailState(), + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), ], eagerError: true); final currentMailboxState = listState.first; final currentEmailState = listState.last; - final newEmailDrafts = await _emailRepository.updateEmailDrafts(accountId, newEmail, oldEmailId); + final newEmailDrafts = await _emailRepository.updateEmailDrafts(session, accountId, newEmail, oldEmailId); yield Right( UpdateEmailDraftsSuccess( newEmailDrafts, diff --git a/lib/features/composer/domain/usecases/upload_attachment_interactor.dart b/lib/features/composer/domain/usecases/upload_attachment_interactor.dart index 52e3a45d51..6abdcf8018 100644 --- a/lib/features/composer/domain/usecases/upload_attachment_interactor.dart +++ b/lib/features/composer/domain/usecases/upload_attachment_interactor.dart @@ -15,7 +15,8 @@ class UploadAttachmentInteractor { FileInfo fileInfo, Uri uploadUri, { CancelToken? cancelToken, - bool isInline = false + bool isInline = false, + bool fromFileShared = false, }) async* { try { final uploadAttachment = await _composerRepository.uploadAttachment( @@ -23,9 +24,17 @@ class UploadAttachmentInteractor { uploadUri, cancelToken: cancelToken ); - yield Right(UploadAttachmentSuccess(uploadAttachment, isInline: isInline)); + yield Right(UploadAttachmentSuccess( + uploadAttachment, + isInline: isInline, + fromFileShared: fromFileShared + )); } catch (e) { - yield Left(UploadAttachmentFailure(e, isInline: isInline)); + yield Left(UploadAttachmentFailure( + e, + isInline: isInline, + fromFileShared: fromFileShared + )); } } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_bindings.dart b/lib/features/composer/presentation/composer_bindings.dart index e6640c54a9..254b3b2294 100644 --- a/lib/features/composer/presentation/composer_bindings.dart +++ b/lib/features/composer/presentation/composer_bindings.dart @@ -1,8 +1,9 @@ import 'package:core/core.dart'; +import 'package:core/utils/file_utils.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; import 'package:tmail_ui_user/features/composer/data/datasource/composer_datasource.dart'; import 'package:tmail_ui_user/features/composer/data/datasource/contact_datasource.dart'; import 'package:tmail_ui_user/features/composer/data/datasource_impl/composer_datasource_impl.dart'; @@ -15,19 +16,20 @@ import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_a import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/upload_attachment_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/email/data/repository/email_repository_impl.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart'; @@ -43,6 +45,12 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_composer_cache_on_web_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identity_interactors_bindings.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart'; import 'package:tmail_ui_user/features/upload/data/datasource/attachment_upload_datasource.dart'; import 'package:tmail_ui_user/features/upload/data/datasource_impl/attachment_upload_datasource_impl.dart'; import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart'; @@ -87,9 +95,17 @@ class ComposerBindings extends BaseBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find(), Get.find())); Get.lazyPut(() => StateDataSourceImpl(Get.find(), Get.find())); + Get.lazyPut(() => EmailHiveCacheDataSourceImpl( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find())); } @override @@ -117,9 +133,12 @@ class ComposerBindings extends BaseBindings { Get.find(), )); Get.lazyPut(() => EmailRepositoryImpl( - Get.find(), - Get.find(), - Get.find(), + { + DataSourceType.network: Get.find(), + DataSourceType.hiveCache: Get.find() + }, + Get.find(), + Get.find(), )); } @@ -135,10 +154,6 @@ class ComposerBindings extends BaseBindings { void bindingsInteractor() { Get.lazyPut(() => LocalFilePickerInteractor()); Get.lazyPut(() => UploadAttachmentInteractor(Get.find())); - Get.lazyPut(() => SendEmailInteractor( - Get.find(), - Get.find() - )); Get.lazyPut(() => SaveEmailAsDraftsInteractor( Get.find(), Get.find())); @@ -149,6 +164,7 @@ class ComposerBindings extends BaseBindings { Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => SaveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => DownloadImageAsBase64Interactor(Get.find())); + Get.lazyPut(() => TransformHtmlEmailContentInteractor(Get.find())); IdentityInteractorsBindings().dependencies(); } @@ -159,7 +175,6 @@ class ComposerBindings extends BaseBindings { Get.lazyPut(() => UploadController(Get.find())); Get.lazyPut(() => RichTextWebController()); Get.lazyPut(() => ComposerController( - Get.find(), Get.find(), Get.find(), Get.find(), @@ -171,6 +186,7 @@ class ComposerBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } diff --git a/lib/features/composer/presentation/composer_controller.dart b/lib/features/composer/presentation/composer_controller.dart index 3282f09691..b0cf800b9c 100644 --- a/lib/features/composer/presentation/composer_controller.dart +++ b/lib/features/composer/presentation/composer_controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:collection/collection.dart'; import 'package:core/core.dart'; @@ -11,8 +12,10 @@ import 'package:filesize/filesize.dart'; import 'package:fk_user_agent/fk_user_agent.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:html_editor_enhanced/html_editor.dart' as web_html_editor; import 'package:http_parser/http_parser.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; @@ -29,26 +32,38 @@ import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:super_tag_editor/tag_editor.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/download_image_as_base64_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_with_device_contact_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/save_email_as_drafts_interactor.dart'; -import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/update_email_drafts_interactor.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/file_upload_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/list_identities_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/compose_action_mode.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/save_to_draft_view_event.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/transform_html_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/remove_composer_cache_on_web_interactor.dart'; @@ -56,6 +71,13 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_co import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/extensions/identity_extension.dart'; +import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' + if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:tmail_ui_user/features/upload/domain/state/local_file_picker_state.dart'; @@ -70,27 +92,27 @@ class ComposerController extends BaseController { final mailboxDashBoardController = Get.find(); final richTextMobileTabletController = Get.find(); + final networkConnectionController = Get.find(); final _appToast = Get.find(); final _imagePaths = Get.find(); final _responsiveUtils = Get.find(); final _uuid = Get.find(); + final _dynamicUrlInterceptors = Get.find(); - final expandModeAttachments = ExpandMode.COLLAPSE.obs; + final expandModeAttachments = ExpandMode.EXPAND.obs; final composerArguments = Rxn(); final isEnableEmailSendButton = false.obs; final isInitialRecipient = false.obs; - final listEmailAddressType = [].obs; final subjectEmail = Rxn(); final screenDisplayMode = ScreenDisplayMode.normal.obs; final toAddressExpandMode = ExpandMode.EXPAND.obs; final ccAddressExpandMode = ExpandMode.EXPAND.obs; final bccAddressExpandMode = ExpandMode.EXPAND.obs; - final identitySelected = Rxn(); - final listIdentities = [].obs; - final emailContentsViewState = Rx>(Right(UIState.idle)); + final emailContentsViewState = Rxn>(); final hasRequestReadReceipt = false.obs; + final ccRecipientState = PrefixRecipientState.disabled.obs; + final bccRecipientState = PrefixRecipientState.disabled.obs; - final SendEmailInteractor _sendEmailInteractor; final LocalFilePickerInteractor _localFilePickerInteractor; final DeviceInfoPlugin _deviceInfoPlugin; final SaveEmailAsDraftsInteractor _saveEmailAsDraftsInteractor; @@ -102,6 +124,7 @@ class ComposerController extends BaseController { final SaveComposerCacheOnWebInteractor _saveComposerCacheOnWebInteractor; final RichTextWebController richTextWebController; final DownloadImageAsBase64Interactor _downloadImageAsBase64Interactor; + final TransformHtmlEmailContentInteractor _transformHtmlEmailContentInteractor; GetAutoCompleteWithDeviceContactInteractor? _getAutoCompleteWithDeviceContactInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; @@ -119,62 +142,36 @@ class ComposerController extends BaseController { final GlobalKey keyToEmailTagEditor = GlobalKey(); final GlobalKey keyCcEmailTagEditor = GlobalKey(); final GlobalKey keyBccEmailTagEditor = GlobalKey(); + final GlobalKey headerEditorMobileWidgetKey = GlobalKey(); + final double defaultPaddingCoordinateYCursorEditor = 8; FocusNode? subjectEmailInputFocusNode; FocusNode? toAddressFocusNode; + FocusNode? ccAddressFocusNode; + FocusNode? bccAddressFocusNode; final RichTextController keyboardRichTextController = RichTextController(); final ScrollController scrollController = ScrollController(); + final ScrollController scrollControllerEmailAddress = ScrollController(); + final ScrollController scrollControllerAttachment = ScrollController(); + + final _saveToDraftEventController = StreamController(); + Stream get _saveToDraftEventStream => _saveToDraftEventController.stream; + late StreamSubscription _saveToDraftStreamSubscription; List initialAttachments = []; String? _textEditorWeb; - List? _emailContents; + String? _initTextEditor; double? maxWithEditor; - late Worker uploadInlineImageWorker; - - void setTextEditorWeb(String? text) => _textEditorWeb = text; - - String? get textEditorWeb => _textEditorWeb; - - HtmlEditorApi? get htmlEditorApi => richTextMobileTabletController.htmlEditorApi; + EmailId? _emailIdEditing; + bool isAttachmentCollapsed = false; + Identity? identitySelected; - void setSubjectEmail(String subject) => subjectEmail.value = subject; - - Future _getEmailBodyText(BuildContext context, { - bool changedEmail = false - }) async { - if (BuildUtils.isWeb) { - var contentHtml = ''; - if (_responsiveUtils.isWebDesktop(context) && - screenDisplayMode.value == ScreenDisplayMode.minimize) { - contentHtml = textEditorWeb ?? ''; - } else { - contentHtml = await richTextWebController.editorController.getText(); - } - log('ComposerController::_getEmailBodyText():WEB: contentHtml: $contentHtml'); - final newContentHtml = contentHtml.removeEditorStartTag(); - log('ComposerController::_getEmailBodyText():WEB: newContentHtml: $newContentHtml'); - return newContentHtml; - } else { - String contentHtml = await htmlEditorApi?.getText() ?? ''; - log('ComposerController::_getEmailBodyText():MOBILE: $contentHtml'); - final newContentHtml = contentHtml.removeEditorStartTag(); - if (changedEmail) { - return newContentHtml; - } else if (_isMobileApp && identitySelected.value?.textSignature?.value.isNotEmpty == true) { - final contentHtmlWithSignature = - '$newContentHtml${identitySelected.value?.textSignature?.value.toSignatureBlock()}'; - log('ComposerController::_getEmailBodyText():MOBILE:SIGNATURE: $contentHtmlWithSignature'); - return contentHtmlWithSignature; - } else { - return newContentHtml; - } - } - } + late Worker uploadInlineImageWorker; + late Worker dashboardViewStateWorker; ComposerController( - this._sendEmailInteractor, this._deviceInfoPlugin, this._localFilePickerInteractor, this._saveEmailAsDraftsInteractor, @@ -186,14 +183,16 @@ class ComposerController extends BaseController { this._saveComposerCacheOnWebInteractor, this.richTextWebController, this._downloadImageAsBase64Interactor, + this._transformHtmlEmailContentInteractor, ); @override void onInit() { super.onInit(); createFocusNodeInput(); - _listenWorker(); - if (!BuildUtils.isWeb) { + scrollControllerEmailAddress.addListener(_scrollControllerEmailAddressListener); + _listenStreamEvent(); + if (PlatformInfo.isMobile) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { await FkUserAgent.init(); }); @@ -205,19 +204,18 @@ class ComposerController extends BaseController { } @override - void onReady() async { + void onReady() { _initEmail(); - _getAllIdentities(); - if (!BuildUtils.isWeb) { - Future.delayed(const Duration(milliseconds: 500), () => - _checkContactPermission()); + if (PlatformInfo.isMobile) { + Future.delayed(const Duration(milliseconds: 500), _checkContactPermission); } super.onReady(); } @override void onClose() { - if (!BuildUtils.isWeb) { + _initTextEditor = null; + if (PlatformInfo.isMobile) { FkUserAgent.release(); } super.onClose(); @@ -229,74 +227,99 @@ class ComposerController extends BaseController { subjectEmailInputFocusNode = null; toAddressFocusNode?.dispose(); toAddressFocusNode = null; + ccAddressFocusNode?.dispose(); + ccAddressFocusNode = null; + bccAddressFocusNode?.dispose(); + bccAddressFocusNode = null; subjectEmailInputController.dispose(); toEmailAddressController.dispose(); ccEmailAddressController.dispose(); bccEmailAddressController.dispose(); uploadInlineImageWorker.dispose(); + dashboardViewStateWorker.dispose(); keyboardRichTextController.dispose(); scrollController.dispose(); + scrollControllerEmailAddress.removeListener(_scrollControllerEmailAddressListener); + scrollControllerEmailAddress.dispose(); + scrollControllerAttachment.dispose(); + _saveToDraftStreamSubscription.cancel(); + _saveToDraftEventController.close(); super.dispose(); } @override - void onData(Either newState) { - super.onData(newState); - newState.map((success) async { - if (success is GetEmailContentLoading) { - emailContentsViewState.value = Right(success); + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetEmailContentLoading || + success is TransformHtmlEmailContentLoading || + success is TransformHtmlEmailContentSuccess) { + emailContentsViewState.value = Right(success); + } else if (success is LocalFilePickerSuccess) { + _pickFileSuccess(success); + } else if (success is GetEmailContentSuccess) { + _getEmailContentSuccess(success); + } else if (success is GetEmailContentFromCacheSuccess) { + _getEmailContentOffLineSuccess(success); + } else if (success is GetAllIdentitiesSuccess) { + _handleGetAllIdentitiesSuccess(success); + } else if (success is DownloadImageAsBase64Success) { + if (PlatformInfo.isWeb) { + richTextWebController.insertImage( + InlineImage( + ImageSource.local, + fileInfo: success.fileInfo, + cid: success.cid, + base64Uri: success.base64Uri)); + } else { + richTextMobileTabletController.insertImage( + InlineImage( + ImageSource.local, + fileInfo: success.fileInfo, + cid: success.cid, + base64Uri: success.base64Uri + ), + fromFileShare: success.fromFileShared + ); } - }); + maxWithEditor = null; + } } @override - void onDone() { - viewState.value.fold( - (failure) { - if (failure is LocalFilePickerFailure || failure is LocalFilePickerCancel) { - _pickFileFailure(failure); - } else if (failure is GetEmailContentFailure) { - emailContentsViewState.value = Left(failure); - } - }, - (success) { - if (success is LocalFilePickerSuccess) { - _pickFileSuccess(success); - } else if (success is GetEmailContentSuccess) { - _getEmailContentSuccess(success); - } else if (success is GetAllIdentitiesSuccess) { - _handleGetAllIdentitiesSuccess(success); - } else if (success is DownloadImageAsBase64Success) { - if(kIsWeb) { - richTextWebController.insertImage( - InlineImage( - ImageSource.local, - fileInfo: success.fileInfo, - cid: success.cid, - base64Uri: success.base64Uri)); - } else { - richTextMobileTabletController.insertImage( - InlineImage( - ImageSource.local, - fileInfo: success.fileInfo, - cid: success.cid, - base64Uri: success.base64Uri)); - } - maxWithEditor = null; - } - }); + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is LocalFilePickerFailure || failure is LocalFilePickerCancel) { + _pickFileFailure(failure); + } else if (failure is GetEmailContentFailure || + failure is TransformHtmlEmailContentFailure) { + emailContentsViewState.value = Left(failure); + } } - void _listenWorker() { + void _listenStreamEvent() { uploadInlineImageWorker = ever(uploadController.uploadInlineViewState, (state) { - log('ComposerController::_listenWorker(): $state'); - if (state is Either) { - state.fold((failure) => null, (success) { - if (success is SuccessAttachmentUploadState) { - _handleUploadInlineSuccess(success); - } - }); - } + log('ComposerController::_listenStreamEvent()::uploadInlineImageWorker: $state'); + state.fold((failure) => null, (success) { + if (success is SuccessAttachmentUploadState) { + _handleUploadInlineSuccess(success); + } + }); + }); + + _saveToDraftStreamSubscription = _saveToDraftEventStream + .debounceTime(const Duration(milliseconds: 300)) + .listen(_handleSaveToDraft); + + dashboardViewStateWorker = ever(mailboxDashBoardController.viewState, (state) { + state.fold((failure) => null, (success) { + if (success is SaveEmailAsDraftsSuccess) { + _emailIdEditing = success.emailAsDrafts.id; + log('ComposerController::_listenStreamEvent::dashboardViewStateWorker:SaveEmailAsDraftsSuccess:emailIdEditing: $_emailIdEditing'); + } else if (success is UpdateEmailDraftsSuccess) { + _emailIdEditing = success.emailAsDrafts.id; + log('ComposerController::_listenStreamEvent::dashboardViewStateWorker:UpdateEmailDraftsSuccess:emailIdEditing: $_emailIdEditing'); + } + }); }); } @@ -311,195 +334,245 @@ class ComposerController extends BaseController { }); } + void _scrollControllerEmailAddressListener() { + if (toEmailAddressController.text.isNotEmpty) { + keyToEmailTagEditor.currentState?.closeSuggestionBox(); + } + if (ccEmailAddressController.text.isNotEmpty) { + keyCcEmailTagEditor.currentState?.closeSuggestionBox(); + } + if (bccEmailAddressController.text.isNotEmpty) { + keyBccEmailTagEditor.currentState?.closeSuggestionBox(); + } + } + void createFocusNodeInput() { toAddressFocusNode = FocusNode(); - subjectEmailInputFocusNode = FocusNode(); + subjectEmailInputFocusNode = FocusNode( + onKey: (focus, event) { + if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { + richTextWebController.editorController.setFocus(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + } + ); + ccAddressFocusNode = FocusNode(); + bccAddressFocusNode = FocusNode(); + + subjectEmailInputFocusNode?.addListener(() { + log('ComposerController::createFocusNodeInput():subjectEmailInputFocusNode: ${subjectEmailInputFocusNode?.hasFocus}'); + if (subjectEmailInputFocusNode?.hasFocus == true) { + if (PlatformInfo.isMobile) { + htmlEditorApi?.unfocus(); + } + _collapseAllRecipient(); + _autoCreateEmailTag(); + } + }); + } + + void onCreatedMobileEditorAction(BuildContext context, HtmlEditorApi editorApi, String? content) { + initTextEditor(content); + richTextMobileTabletController.htmlEditorApi = editorApi; + keyboardRichTextController.onCreateHTMLEditor( + editorApi, + onEnterKeyDown: _onEnterKeyDown, + context: context, + onFocus: _onEditorFocusOnMobile, + onChangeCursor: (coordinates) { + _onChangeCursorOnMobile(coordinates, context); + }, + ); + } + + void onLoadCompletedMobileEditorAction(HtmlEditorApi editorApi, WebUri? url) { + if (identitySelected == null) { + _getAllIdentities(); + } } void _initEmail() { - final arguments = kIsWeb ? mailboxDashBoardController.routerArguments : Get.arguments; + final arguments = PlatformInfo.isWeb + ? mailboxDashBoardController.composerArguments + : Get.arguments; if (arguments is ComposerArguments) { composerArguments.value = arguments; + injectAutoCompleteBindings( - mailboxDashBoardController.sessionCurrent, - mailboxDashBoardController.accountId.value); + mailboxDashBoardController.sessionCurrent, + mailboxDashBoardController.accountId.value + ); - if (arguments.emailActionType == EmailActionType.edit) { - _getEmailContentAction(arguments); + switch(arguments.emailActionType) { + case EmailActionType.editDraft: + _initEmailAddress( + presentationEmail: arguments.presentationEmail!, + actionType: EmailActionType.editDraft + ); + _initSubjectEmail( + presentationEmail: arguments.presentationEmail!, + actionType: EmailActionType.editDraft + ); + _getEmailContentFromEmailId( + emailId: arguments.presentationEmail!.id!, + isDraftEmail: arguments.presentationEmail!.isDraft + ); + _emailIdEditing = arguments.presentationEmail!.id!; + break; + case EmailActionType.editSendingEmail: + _initEmailAddress( + presentationEmail: arguments.sendingEmail!.presentationEmail, + actionType: EmailActionType.editSendingEmail + ); + _initSubjectEmail( + presentationEmail: arguments.sendingEmail!.presentationEmail, + actionType: EmailActionType.editSendingEmail + ); + _getEmailContentFromSendingEmail(arguments.sendingEmail!); + _emailIdEditing = arguments.sendingEmail!.presentationEmail.id!; + break; + case EmailActionType.composeFromContentShared: + _getEmailContentFromContentShared(arguments.emailContents!); + break; + case EmailActionType.composeFromFileShared: + _addAttachmentFromFileShare(arguments.listSharedMediaFile!); + break; + case EmailActionType.composeFromEmailAddress: + listToEmailAddress.add(arguments.emailAddress!); + isInitialRecipient.value = true; + toAddressExpandMode.value = ExpandMode.COLLAPSE; + _updateStatusEmailSendButton(); + break; + case EmailActionType.composeFromMailtoUri: + if (arguments.subject != null) { + setSubjectEmail(arguments.subject!); + subjectEmailInputController.text = arguments.subject!; + } + if (arguments.emailAddress != null) { + listToEmailAddress.add(arguments.emailAddress!); + isInitialRecipient.value = true; + toAddressExpandMode.value = ExpandMode.COLLAPSE; + } + _updateStatusEmailSendButton(); + break; + case EmailActionType.reply: + case EmailActionType.replyAll: + _initEmailAddress( + presentationEmail: arguments.presentationEmail!, + actionType: arguments.emailActionType, + mailboxRole: arguments.presentationEmail!.mailboxContain?.role ?? mailboxDashBoardController.selectedMailbox.value?.role + ); + _initSubjectEmail( + presentationEmail: arguments.presentationEmail!, + actionType: arguments.emailActionType + ); + _transformHtmlEmailContent(arguments.emailContents); + break; + case EmailActionType.forward: + _initSubjectEmail( + presentationEmail: arguments.presentationEmail!, + actionType: arguments.emailActionType + ); + _initAttachments(arguments.attachments ?? []); + _transformHtmlEmailContent(arguments.emailContents); + break; + case EmailActionType.reopenComposerBrowser: + _initEmailAddress( + presentationEmail: arguments.presentationEmail!, + actionType: EmailActionType.reopenComposerBrowser + ); + _initSubjectEmail( + presentationEmail: arguments.presentationEmail!, + actionType: EmailActionType.reopenComposerBrowser + ); + _initAttachments(arguments.attachments ?? []); + _getEmailContentFromSessionStorageBrowser(arguments.emailContents!); + break; + default: + break; } - - _initEmailAddress(arguments); - _initSubjectEmail(arguments); - _initAttachments(arguments); } _autoFocusFieldWhenLauncher(); } - void _initSubjectEmail(ComposerArguments arguments) { - if (currentContext != null) { - final subjectEmail = arguments.presentationEmail?.getEmailTitle().trim() ?? ''; - final newSubject = arguments.emailActionType.getSubjectComposer(currentContext!, subjectEmail); - setSubjectEmail(newSubject); - subjectEmailInputController.text = newSubject; - } + void _initSubjectEmail({ + required PresentationEmail presentationEmail, + required EmailActionType actionType + }) { + final subjectEmail = presentationEmail.getEmailTitle().trim(); + final newSubject = actionType.getSubjectComposer(currentContext, subjectEmail); + setSubjectEmail(newSubject); + subjectEmailInputController.text = newSubject; } - void _initAttachments(ComposerArguments arguments) { - if (arguments.attachments?.isNotEmpty == true) { - initialAttachments = arguments.attachments!; - uploadController.initializeUploadAttachments( - arguments.attachments!.listAttachmentsDisplayedOutSide); - } - if (BuildUtils.isWeb) { - expandModeAttachments.value = ExpandMode.EXPAND; + void _initAttachments(List attachments) { + if (attachments.isNotEmpty) { + initialAttachments = attachments; + uploadController.initializeUploadAttachments(attachments.listAttachmentsDisplayedOutSide); } } void _getAllIdentities() { final accountId = mailboxDashBoardController.accountId.value; - if (accountId != null) { - consumeState(_getAllIdentitiesInteractor.execute(accountId)); + final session = mailboxDashBoardController.sessionCurrent; + if (accountId != null && session != null) { + consumeState(_getAllIdentitiesInteractor.execute(session, accountId)); } } void _handleGetAllIdentitiesSuccess(GetAllIdentitiesSuccess success) async { - if (success.identities?.isNotEmpty == true) { - listIdentities.value = success.identities! - .where((identity) => identity.mayDelete == true) - .toList(); - - if (listIdentities.isNotEmpty) { - await selectIdentity(listIdentities.first); - } + final listIdentitiesMayDeleted = success.identities?.toListMayDeleted() ?? []; + if (listIdentitiesMayDeleted.isNotEmpty) { + await _selectIdentity(listIdentitiesMayDeleted.first); } _autoFocusFieldWhenLauncher(); } - String? getContentEmail(BuildContext context) { - if (composerArguments.value != null) { - switch(composerArguments.value!.emailActionType) { - case EmailActionType.reply: - case EmailActionType.forward: - case EmailActionType.replyAll: - return getEmailContentQuotedAsHtml(context, composerArguments.value!); - case EmailActionType.edit: - return getEmailContentDraftsAsHtml(); - default: - return ''; - } - } - return ''; - } - - String? _getHeaderEmailQuoted(BuildContext context, ComposerArguments arguments) { - final presentationEmail = arguments.presentationEmail; - if (presentationEmail != null) { - final locale = Localizations.localeOf(context).toLanguageTag(); - log('ComposerController::_getHeaderEmailQuoted(): emailActionType: ${arguments.emailActionType}'); - switch(arguments.emailActionType) { - case EmailActionType.reply: - case EmailActionType.replyAll: - final receivedAt = presentationEmail.receivedAt; - final emailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true); - return AppLocalizations.of(context).header_email_quoted( - receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale), - emailAddress); - case EmailActionType.forward: - var headerQuoted = '------- ${AppLocalizations.of(context).forwarded_message} -------'.addNewLineTag(); - - final subject = presentationEmail.subject ?? ''; - final receivedAt = presentationEmail.receivedAt; - final fromEmailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true); - final toEmailAddress = presentationEmail.to.listEmailAddressToString(isFullEmailAddress: true); - final ccEmailAddress = presentationEmail.cc.listEmailAddressToString(isFullEmailAddress: true); - final bccEmailAddress = presentationEmail.bcc.listEmailAddressToString(isFullEmailAddress: true); - - if (subject.isNotEmpty) { - headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).subject_email}: ') - .append(subject) - .addNewLineTag(); - } - if (receivedAt != null) { - headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).date}: ') - .append(receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale)) - .addNewLineTag(); - } - if (fromEmailAddress.isNotEmpty) { - headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).from_email_address_prefix}: ') - .append(fromEmailAddress) - .addNewLineTag(); - } - if (toEmailAddress.isNotEmpty) { - headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).to_email_address_prefix}: ') - .append(toEmailAddress) - .addNewLineTag(); - } - if (ccEmailAddress.isNotEmpty) { - headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).cc_email_address_prefix}: ') - .append(ccEmailAddress) - .addNewLineTag(); - } - if (bccEmailAddress.isNotEmpty) { - headerQuoted = headerQuoted - .append('${AppLocalizations.of(context).bcc_email_address_prefix}: ') - .append(bccEmailAddress) - .addNewLineTag(); - } - - return headerQuoted; - default: - return null; - } - } - return null; - } - - void _initEmailAddress(ComposerArguments arguments) { + void _initEmailAddress({ + required PresentationEmail presentationEmail, + required EmailActionType actionType, + Role? mailboxRole, + }) { + final recipients = presentationEmail.generateRecipientsEmailAddressForComposer( + emailActionType: actionType, + mailboxRole: mailboxRole + ); final userProfile = mailboxDashBoardController.userProfile.value; - if (arguments.presentationEmail != null && userProfile != null) { - final userEmailAddress = EmailAddress(null, userProfile.email); - - final recipients = arguments.presentationEmail!.generateRecipientsEmailAddressForComposer( - arguments.emailActionType, - arguments.mailboxRole); - - if (arguments.mailboxRole == PresentationMailbox.roleSent - || arguments.emailActionType == EmailActionType.edit) { - listToEmailAddress = List.from(recipients.value1); - listCcEmailAddress = List.from(recipients.value2); - listBccEmailAddress = List.from(recipients.value3); + if (userProfile != null) { + final isSender = presentationEmail.from.asList().every((element) => element.email == userProfile.email); + if (isSender) { + listToEmailAddress = List.from(recipients.value1.toSet()); + listCcEmailAddress = List.from(recipients.value2.toSet()); + listBccEmailAddress = List.from(recipients.value3.toSet()); } else { - listToEmailAddress = List.from(recipients.value1.toSet().filterEmailAddress(userEmailAddress)); - listCcEmailAddress = List.from(recipients.value2.toSet().filterEmailAddress(userEmailAddress)); - listBccEmailAddress = List.from(recipients.value3.toSet().filterEmailAddress(userEmailAddress)); - } - - if (listToEmailAddress.isNotEmpty || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty) { - isInitialRecipient.value = true; - toAddressExpandMode.value = ExpandMode.COLLAPSE; - } - - if (listCcEmailAddress.isNotEmpty) { - listEmailAddressType.add(PrefixEmailAddress.cc); - ccAddressExpandMode.value = ExpandMode.COLLAPSE; + listToEmailAddress = List.from(recipients.value1.toSet().filterEmailAddress(userProfile.email)); + listCcEmailAddress = List.from(recipients.value2.toSet().filterEmailAddress(userProfile.email)); + listBccEmailAddress = List.from(recipients.value3.toSet().filterEmailAddress(userProfile.email)); } + } else { + listToEmailAddress = List.from(recipients.value1.toSet()); + listCcEmailAddress = List.from(recipients.value2.toSet()); + listBccEmailAddress = List.from(recipients.value3.toSet()); + } - if (listBccEmailAddress.isNotEmpty) { - listEmailAddressType.add(PrefixEmailAddress.bcc); - bccAddressExpandMode.value = ExpandMode.COLLAPSE; - } - } else if (arguments.emailAddress != null) { - listToEmailAddress.add(arguments.emailAddress!); + if (listToEmailAddress.isNotEmpty || listCcEmailAddress.isNotEmpty || listBccEmailAddress.isNotEmpty) { isInitialRecipient.value = true; toAddressExpandMode.value = ExpandMode.COLLAPSE; } + + if (listCcEmailAddress.isNotEmpty) { + ccRecipientState.value = PrefixRecipientState.enabled; + ccAddressExpandMode.value = ExpandMode.COLLAPSE; + } + + if (listBccEmailAddress.isNotEmpty) { + bccRecipientState.value = PrefixRecipientState.enabled; + bccAddressExpandMode.value = ExpandMode.COLLAPSE; + } + _updateStatusEmailSendButton(); } @@ -533,41 +606,53 @@ class ComposerController extends BaseController { } } - String getEmailContentQuotedAsHtml(BuildContext context, ComposerArguments arguments) { - final headerEmailQuoted = _getHeaderEmailQuoted(context, arguments); + String getEmailContentQuotedAsHtml({ + required BuildContext context, + required String emailContent, + required EmailActionType emailActionType, + required PresentationEmail presentationEmail, + }) { + final headerEmailQuoted = emailActionType.getHeaderEmailQuoted( + context: context, + presentationEmail: presentationEmail + ); log('ComposerController::getEmailContentQuotedAsHtml(): headerEmailQuoted: $headerEmailQuoted'); - final headerEmailQuotedAsHtml = headerEmailQuoted != null ? headerEmailQuoted.addBlockTag('cite') : ''; - - final trustAsHtml = arguments.emailContents?.asHtmlString ?? ''; - final emailQuotedHtml = '


$headerEmailQuotedAsHtml${trustAsHtml.addBlockQuoteTag()}


'; + final headerEmailQuotedAsHtml = headerEmailQuoted != null + ? headerEmailQuoted.addCiteTag() + : ''; + final emailQuotedHtml = '${HtmlExtension.editorStartTags}$headerEmailQuotedAsHtml${emailContent.addBlockQuoteTag()}'; return emailQuotedHtml; } Future _generateEmail( - BuildContext context, - UserProfile userProfile, - { - bool asDrafts = false, - MailboxId? draftMailboxId, - MailboxId? outboxMailboxId - } + BuildContext context, + UserProfile userProfile, + { + bool asDrafts = false, + MailboxId? draftMailboxId, + MailboxId? outboxMailboxId, + ComposerArguments? arguments, + } ) async { Set listFromEmailAddress = {EmailAddress(null, userProfile.email)}; - if (identitySelected.value?.email?.isNotEmpty == true) { - listFromEmailAddress = {EmailAddress( - identitySelected.value?.name, - identitySelected.value?.email)}; + if (identitySelected?.email?.isNotEmpty == true) { + listFromEmailAddress = { + EmailAddress( + identitySelected?.name, + identitySelected?.email + ) + }; } Set listReplyToEmailAddress = {EmailAddress(null, userProfile.email)}; - if (identitySelected.value?.replyTo?.isNotEmpty == true) { - listReplyToEmailAddress = identitySelected.value!.replyTo!; + if (identitySelected?.replyTo?.isNotEmpty == true) { + listReplyToEmailAddress = identitySelected!.replyTo!; } final attachments = {}; attachments.addAll(uploadController.generateAttachments() ?? []); - var emailBodyText = await _getEmailBodyText(context); + var emailBodyText = await _getEmailBodyText(context, asDrafts: asDrafts); if (uploadController.mapInlineAttachments.isNotEmpty) { final mapContents = await _getMapContent(emailBodyText); emailBodyText = mapContents.value1; @@ -595,17 +680,20 @@ class ComposerController extends BaseController { mapKeywords[KeyWordIdentifier.emailSeen] = true; } - final generateEmailId = EmailId(Id(_uuid.v1())); + final inReplyTo = _generateInReplyTo(arguments); + final references = _generateReferences(arguments); + final generatePartId = PartId(_uuid.v1()); return Email( - generateEmailId, mailboxIds: mailboxIds.isNotEmpty ? mailboxIds : null, from: listFromEmailAddress, to: listToEmailAddress.toSet(), cc: listCcEmailAddress.toSet(), bcc: listBccEmailAddress.toSet(), replyTo: listReplyToEmailAddress, + inReplyTo: inReplyTo, + references: references, keywords: mapKeywords.isNotEmpty ? mapKeywords : null, subject: subjectEmail.value, htmlBody: { @@ -622,6 +710,32 @@ class ComposerController extends BaseController { ); } + MessageIdsHeaderValue? _generateInReplyTo(ComposerArguments? arguments) { + if (arguments?.emailActionType == EmailActionType.reply || + arguments?.emailActionType == EmailActionType.replyAll) { + return arguments?.messageId; + } + return null; + } + + MessageIdsHeaderValue? _generateReferences(ComposerArguments? arguments) { + if (arguments?.emailActionType == EmailActionType.reply || + arguments?.emailActionType == EmailActionType.replyAll || + arguments?.emailActionType == EmailActionType.forward) { + Set ids = {}; + if (arguments?.messageId?.ids.isNotEmpty == true) { + ids.addAll(arguments!.messageId!.ids); + } + if (arguments?.references?.ids.isNotEmpty == true) { + ids.addAll(arguments!.references!.ids); + } + if (ids.isNotEmpty) { + return MessageIdsHeaderValue(ids); + } + } + return null; + } + Future>> _getMapContent(String emailBodyText) async { if (kIsWeb) { return await richTextWebController.refactorContentHasInlineImage( @@ -652,6 +766,13 @@ class ComposerController extends BaseController { void sendEmailAction(BuildContext context) async { clearFocusEditor(context); + if (toEmailAddressController.text.isNotEmpty + || ccEmailAddressController.text.isNotEmpty + || bccEmailAddressController.text.isNotEmpty) { + _collapseAllRecipient(); + _autoCreateEmailTag(); + } + if (!isEnableEmailSendButton.value) { showConfirmDialogAction(context, AppLocalizations.of(context).message_dialog_send_email_without_recipient, @@ -722,42 +843,134 @@ class ComposerController extends BaseController { } void _handleSendMessages(BuildContext context) async { + final session = mailboxDashBoardController.sessionCurrent; final arguments = composerArguments.value; final accountId = mailboxDashBoardController.accountId.value; final sentMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleSent]; final outboxMailboxId = mailboxDashBoardController.outboxMailbox?.id; final userProfile = mailboxDashBoardController.userProfile.value; - if (arguments != null && accountId != null && userProfile != null) { - final email = await _generateEmail(context, userProfile, outboxMailboxId: outboxMailboxId); - final submissionCreateId = Id(_uuid.v1()); - mailboxDashBoardController.consumeState( - _sendEmailInteractor.execute( - accountId, - EmailRequest( - email, - submissionCreateId, - sentMailboxId: sentMailboxId, - identity: identitySelected.value, - emailIdDestroyed: arguments.emailActionType == EmailActionType.edit - ? arguments.presentationEmail?.id - : null), - mailboxRequest: outboxMailboxId == null - ? CreateNewMailboxRequest( - Id(_uuid.v1()), - PresentationMailbox.outboxMailboxName) - : null - ) + if (arguments != null && accountId != null && userProfile != null && session != null) { + final createdEmail = await _generateEmail( + context, + userProfile, + outboxMailboxId: outboxMailboxId, + arguments: arguments ); + final emailRequest = arguments.emailActionType == EmailActionType.editSendingEmail + ? arguments.sendingEmail!.toEmailRequest(newEmail: createdEmail) + : EmailRequest( + email: createdEmail, + sentMailboxId: sentMailboxId, + identityId: identitySelected?.id, + emailIdDestroyed: arguments.emailActionType == EmailActionType.editDraft + ? arguments.presentationEmail?.id + : null, + emailIdAnsweredOrForwarded: arguments.presentationEmail?.id, + emailActionType: arguments.emailActionType + ); + final mailboxRequest = outboxMailboxId == null + ? CreateNewMailboxRequest(Id(_uuid.v1()), PresentationMailbox.outboxMailboxName) + : null; + + if (PlatformInfo.isAndroid && !networkConnectionController.isNetworkConnectionAvailable()) { + switch(arguments.sendingEmailActionType) { + case SendingEmailActionType.create: + _showConfirmDialogStoreSendingEmail( + SendingEmailArguments( + session, + accountId, + emailRequest, + mailboxRequest, + actionMode: ComposeActionMode.pushQueue + ) + ); + break; + case SendingEmailActionType.edit: + uploadController.clearInlineFileUploaded(); + _closeComposerAction( + result: SendingEmailArguments( + session, + accountId, + emailRequest, + mailboxRequest, + actionMode: ComposeActionMode.editQueue + ) + ); + break; + default: + uploadController.clearInlineFileUploaded(); + _closeComposerAction( + result: SendingEmailArguments( + session, + accountId, + emailRequest, + mailboxRequest, + ) + ); + } + } else { + uploadController.clearInlineFileUploaded(); + _closeComposerAction( + result: SendingEmailArguments( + session, + accountId, + emailRequest, + mailboxRequest, + ) + ); + } + } else { + logError('ComposerController::_handleSendMessages: arguments/accountId/userProfile/session is NULL'); uploadController.clearInlineFileUploaded(); + _closeComposerAction(); } + } - if (kIsWeb) { - closeComposerWeb(); - } else { - popBack(); - } + void _showConfirmDialogStoreSendingEmail(SendingEmailArguments sendingEmailArguments) { + showConfirmDialogAction( + currentContext!, + '', + AppLocalizations.of(currentContext!).proceed, + onConfirmAction: () { + uploadController.clearInlineFileUploaded(); + _closeComposerAction(result: sendingEmailArguments); + }, + onCancelAction: () { + uploadController.clearInlineFileUploaded(); + _closeComposerAction(); + }, + title: AppLocalizations.of(currentContext!).youAreInOfflineMode, + icon: SvgPicture.asset(_imagePaths.icDialogOfflineMode), + alignCenter: true, + messageStyle: const TextStyle( + color: AppColor.colorTitleSendingItem, + fontSize: 15, + fontWeight: FontWeight.normal + ), + listTextSpan: [ + TextSpan(text: AppLocalizations.of(currentContext!).messageDialogWhenStoreSendingEmailFirst), + TextSpan( + text: AppLocalizations.of(currentContext!).messageDialogWhenStoreSendingEmailSecond, + style: const TextStyle( + color: AppColor.colorTitleSendingItem, + fontSize: 15, + fontWeight: FontWeight.w600 + ), + ), + TextSpan(text: AppLocalizations.of(currentContext!).messageDialogWhenStoreSendingEmailThird), + TextSpan( + text: AppLocalizations.of(currentContext!).sendingQueue, + style: const TextStyle( + color: AppColor.colorTitleSendingItem, + fontSize: 15, + fontWeight: FontWeight.w600 + ), + ), + TextSpan(text: AppLocalizations.of(currentContext!).messageDialogWhenStoreSendingEmailTail) + ] + ); } void _checkContactPermission() async { @@ -803,9 +1016,8 @@ class ComposerController extends BaseController { .execute(AutoCompletePattern(word: word, accountId: accountId)) .then((value) => value.fold( (failure) => [], - (success) => success is GetAutoCompleteSuccess - ? success.listEmailAddress - : [])); + (success) => _getAutoCompleteSuccess(success, word) + )); return listEmailAddress; } else { if (_getAutoCompleteInteractor == null) { @@ -815,14 +1027,33 @@ class ComposerController extends BaseController { final listEmailAddress = await _getAutoCompleteInteractor! .execute(AutoCompletePattern(word: word, accountId: accountId)) .then((value) => value.fold( - (failure) => [], - (success) => success is GetAutoCompleteSuccess - ? success.listEmailAddress - : [])); + (failure) => [], + (success) => _getAutoCompleteSuccess(success, word) + )); return listEmailAddress; } } + List _getAutoCompleteSuccess(Success success, String word) { + if (success is GetAutoCompleteSuccess) { + if (success.listEmailAddress.isEmpty == true && GetUtils.isEmail(word)) { + final unknownEmailAddress = EmailAddress(word, word); + return [unknownEmailAddress]; + } + if (GetUtils.isEmail(word)) { + bool isContainsTypedEmail = success.listEmailAddress.any((emailAddress) => emailAddress.email == word); + if (!isContainsTypedEmail) { + final unknownEmailAddress = EmailAddress(word, word); + success.listEmailAddress.insert(0, unknownEmailAddress); + return success.listEmailAddress; + } + } + return success.listEmailAddress; + } else { + return []; + } + } + void openPickAttachmentMenu(BuildContext context, List actionTiles) { clearFocusEditor(context); @@ -844,8 +1075,10 @@ class ComposerController extends BaseController { void _pickFileFailure(Failure failure) { if (failure is LocalFilePickerFailure) { - if (currentContext != null) { - _appToast.showErrorToast(AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments); + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments); } } } @@ -871,7 +1104,7 @@ class ComposerController extends BaseController { final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { - final uploadUri = session.getUploadUri(accountId); + final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl); uploadController.justUploadAttachmentsAction(pickedFiles, uploadUri); } } @@ -880,49 +1113,40 @@ class ComposerController extends BaseController { uploadController.deleteFileUploaded(uploadId); } - Future _isEmailChanged( - BuildContext context, - ComposerArguments arguments, - ) async { - final newEmailBody = await _getEmailBodyText(context, changedEmail: true); + Future _isEmailChanged({ + required BuildContext context, + required EmailActionType emailActionType, + PresentationEmail? presentationEmail, + Role? mailboxRole, + }) async { + final newEmailBody = await _getEmailBodyText(context, asDrafts: true); log('ComposerController::_isEmailChanged(): newEmailBody: $newEmailBody'); - var oldEmailBody = getContentEmail(context) ?? ''; - log('ComposerController::_isEmailChanged(): getContentEmail: $oldEmailBody'); - if (arguments.emailActionType != EmailActionType.compose && - oldEmailBody.isNotEmpty) { - oldEmailBody = BuildUtils.isWeb ? oldEmailBody : '\n$oldEmailBody\n'; - } - if (BuildUtils.isWeb) { - if (identitySelected.value?.htmlSignature?.value.isNotEmpty == true) { - oldEmailBody = '$oldEmailBody${identitySelected.value?.htmlSignature?.value.toSignatureBlock()}'; - } else if (identitySelected.value?.textSignature?.value.isNotEmpty == true) { - oldEmailBody = '$oldEmailBody${identitySelected.value?.textSignature?.value.toSignatureBlock()}'; - } - } - + final oldEmailBody = _initTextEditor ?? ''; log('ComposerController::_isEmailChanged(): oldEmailBody: $oldEmailBody'); final isEmailBodyChanged = !oldEmailBody.trim().isSame(newEmailBody.trim()); log('ComposerController::_isEmailChanged(): isEmailBodyChanged: $isEmailBodyChanged'); - - final newEmailSubject = subjectEmail.value; - final titleEmail = arguments.presentationEmail?.getEmailTitle().trim() ?? ''; - final oldEmailSubject = arguments.emailActionType.getSubjectComposer(currentContext!, titleEmail); - final isEmailSubjectChanged = !oldEmailSubject.isSame(newEmailSubject); - - final recipients = arguments.presentationEmail - ?.generateRecipientsEmailAddressForComposer(arguments.emailActionType, arguments.mailboxRole) - ?? const Tuple3([], [], []); + final newEmailSubject = subjectEmail.value ?? ''; + final oldEmailSubject = emailActionType == EmailActionType.editDraft + ? presentationEmail?.getEmailTitle().trim() ?? '' + : ''; + final isEmailSubjectChanged = !oldEmailSubject.trim().isSame(newEmailSubject.trim()); + + final recipients = presentationEmail + ?.generateRecipientsEmailAddressForComposer( + emailActionType: emailActionType, + mailboxRole: mailboxRole + ) ?? const Tuple3([], [], []); final newToEmailAddress = listToEmailAddress; - final oldToEmailAddress = recipients.value1; + final oldToEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value1 : []; final isToEmailAddressChanged = !oldToEmailAddress.isSame(newToEmailAddress); - final newCcEmailAddress = listToEmailAddress; - final oldCcEmailAddress = recipients.value1; + final newCcEmailAddress = listCcEmailAddress; + final oldCcEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value2 : []; final isCcEmailAddressChanged = !oldCcEmailAddress.isSame(newCcEmailAddress); - final newBccEmailAddress = listToEmailAddress; - final oldBccEmailAddress = recipients.value1; + final newBccEmailAddress = listBccEmailAddress; + final oldBccEmailAddress = emailActionType == EmailActionType.editDraft ? recipients.value3 : []; final isBccEmailAddressChanged = !oldBccEmailAddress.isSame(newBccEmailAddress); final isAttachmentsChanged = !initialAttachments.isSame(uploadController.attachmentsUploaded.toList()); @@ -936,43 +1160,130 @@ class ComposerController extends BaseController { return false; } - void saveEmailAsDrafts(BuildContext context, {bool canPop = true}) async { + void saveToDraftAndClose(BuildContext context, {bool canPop = true}) async { + log('ComposerController::saveToDraftAndClose:'); clearFocusEditor(context); final arguments = composerArguments.value; - final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts]; final userProfile = mailboxDashBoardController.userProfile.value; final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; - if (arguments != null && userProfile != null && accountId != null) { - final isChanged = await _isEmailChanged(context, arguments); - if (isChanged) { - final newEmail = await _generateEmail( - context, - userProfile, - asDrafts: true, - draftMailboxId: draftMailboxId); - final oldEmail = arguments.presentationEmail; - - if (arguments.emailActionType == EmailActionType.edit && oldEmail != null) { - mailboxDashBoardController.consumeState( - _updateEmailDraftsInteractor.execute(accountId, newEmail, oldEmail.id)); - } else { - mailboxDashBoardController.consumeState( - _saveEmailAsDraftsInteractor.execute(accountId, newEmail)); - } + if (arguments == null || + userProfile == null || + session == null || + accountId == null || + arguments.presentationEmail?.id != _emailIdEditing + ) { + if (PlatformInfo.isWeb) { + mailboxDashBoardController.closeComposerOverlay(); + } else { + if (canPop) popBack(); + } + return; + } - uploadController.clearInlineFileUploaded(); + final isChanged = await _isEmailChanged( + context: context, + emailActionType: arguments.emailActionType, + presentationEmail: arguments.presentationEmail, + mailboxRole: arguments.mailboxRole + ); + + if (isChanged && context.mounted) { + final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts]; + + final newEmail = await _generateEmail( + context, + userProfile, + asDrafts: true, + draftMailboxId: draftMailboxId, + arguments: arguments, + ); + + if (arguments.emailActionType == EmailActionType.editDraft) { + mailboxDashBoardController.consumeState( + _updateEmailDraftsInteractor.execute( + session, + accountId, + newEmail, + arguments.presentationEmail!.id! + ) + ); + } else { + mailboxDashBoardController.consumeState( + _saveEmailAsDraftsInteractor.execute( + session, + accountId, + newEmail + ) + ); } + + uploadController.clearInlineFileUploaded(); } - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { mailboxDashBoardController.closeComposerOverlay(); } else { if (canPop) popBack(); } } + void saveToDraftAction(BuildContext context) { + final userProfile = mailboxDashBoardController.userProfile.value; + final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; + final draftMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleDrafts]; + + if (draftMailboxId == null || userProfile == null || session == null || accountId == null) { + logError('ComposerController::saveToDraftAction: Param is NULL'); + return; + } + + _saveToDraftEventController.add( + SaveToDraftViewEvent( + context: context, + session: session, + accountId: accountId, + userProfile: userProfile, + draftMailboxId: draftMailboxId, + emailIdEditing: _emailIdEditing, + arguments: mailboxDashBoardController.composerArguments, + ) + ); + } + + void _handleSaveToDraft(SaveToDraftViewEvent event) async { + log('ComposerController::_handleSaveToDraft:emailIdEditing: ${event.emailIdEditing}'); + final newEmail = await _generateEmail( + event.context, + event.userProfile, + asDrafts: true, + draftMailboxId: event.draftMailboxId, + arguments: event.arguments + ); + + if (event.emailIdEditing == null) { + mailboxDashBoardController.consumeState( + _saveEmailAsDraftsInteractor.execute( + event.session, + event.accountId, + newEmail + ) + ); + } else { + mailboxDashBoardController.consumeState( + _updateEmailDraftsInteractor.execute( + event.session, + event.accountId, + newEmail, + event.emailIdEditing! + ) + ); + } + } + File _covertSharedMediaFileToFile(SharedMediaFile sharedMediaFile) { return File( Platform.isIOS @@ -1022,67 +1333,102 @@ class ComposerController extends BaseController { return listFileInfo; } - void _getEmailContentAction(ComposerArguments arguments) async { - - final listSharedMediaFile = arguments.listSharedMediaFile; - if(listSharedMediaFile != null && listSharedMediaFile.isNotEmpty) { - final listImageSharedMediaFile = listSharedMediaFile.where((element) => element.type == SharedMediaType.IMAGE); - final listFileAttachmentSharedMediaFile = listSharedMediaFile.where((element) => element.type != SharedMediaType.IMAGE); - if (listImageSharedMediaFile.isNotEmpty) { - final listInlineImage = covertListSharedMediaFileToInlineImage(arguments.listSharedMediaFile!); - for (var e in listInlineImage) { - _uploadInlineAttachmentsAction(e.fileInfo!); - } + void _addAttachmentFromFileShare(List listSharedMediaFile) { + final listImageSharedMediaFile = listSharedMediaFile.where((element) => element.type == SharedMediaType.IMAGE); + final listFileAttachmentSharedMediaFile = listSharedMediaFile.where((element) => element.type != SharedMediaType.IMAGE); + if (listImageSharedMediaFile.isNotEmpty) { + final listInlineImage = covertListSharedMediaFileToInlineImage(listSharedMediaFile); + for (var e in listInlineImage) { + _uploadInlineAttachmentsAction(e.fileInfo!, fromFileShared: true); } - if (listFileAttachmentSharedMediaFile.isNotEmpty) { - final listFile = covertListSharedMediaFileToFileInfo(arguments.listSharedMediaFile!); - if (uploadController.hasEnoughMaxAttachmentSize(listFiles: listFile)) { - _uploadAttachmentsAction(listFile); - } else { - if (currentContext != null) { - showConfirmDialogAction( - currentContext!, - AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( - filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), - AppLocalizations.of(currentContext!).got_it, - onConfirmAction: () => {}, - title: AppLocalizations.of(currentContext!).maximum_files_size, - hasCancelButton: false, - ); - } + } + if (listFileAttachmentSharedMediaFile.isNotEmpty) { + final listFile = covertListSharedMediaFileToFileInfo(listSharedMediaFile); + if (uploadController.hasEnoughMaxAttachmentSize(listFiles: listFile)) { + _uploadAttachmentsAction(listFile); + } else { + if (currentContext != null) { + showConfirmDialogAction( + currentContext!, + AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( + filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), + AppLocalizations.of(currentContext!).got_it, + title: AppLocalizations.of(currentContext!).maximum_files_size, + hasCancelButton: false, + ); } } } + } - if(arguments.emailContents != null && arguments.emailContents!.isNotEmpty) { - _emailContents = arguments.emailContents; - emailContentsViewState.value = Right(GetEmailContentSuccess(_emailContents!, [], [], null)); - } else { - final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(); - final accountId = mailboxDashBoardController.sessionCurrent?.accounts.keys.first; - final emailId = arguments.presentationEmail?.id; - if (emailId != null && baseDownloadUrl != null && accountId != null) { - consumeState(_getEmailContentInteractor.execute(accountId, emailId, baseDownloadUrl)); - } + void _getEmailContentFromSendingEmail(SendingEmail sendingEmail) { + consumeState(Stream.value( + Right(GetEmailContentSuccess( + htmlEmailContent: sendingEmail.presentationEmail.emailContentList.asHtmlString, + attachments: sendingEmail.email.allAttachments, + emailCurrent: sendingEmail.email + )) + )); + } + + void _getEmailContentFromSessionStorageBrowser(String content) { + consumeState(Stream.value( + Right(GetEmailContentSuccess( + htmlEmailContent: content, + attachments: [], + )) + )); + } + + void _getEmailContentFromContentShared(String content) { + consumeState(Stream.value( + Right(GetEmailContentSuccess( + htmlEmailContent: content, + attachments: [], + )) + )); + } + + void _getEmailContentFromEmailId({required EmailId emailId, bool isDraftEmail = false}) { + final session = mailboxDashBoardController.sessionCurrent; + final accountId = mailboxDashBoardController.accountId.value; + if (session != null && accountId != null) { + consumeState(_getEmailContentInteractor.execute( + session, + accountId, + emailId, + mailboxDashBoardController.baseDownloadUrl, + TransformConfiguration.empty() + )); } } + void _getEmailContentOffLineSuccess(GetEmailContentFromCacheSuccess success) { + _initAttachments(success.attachments); + emailContentsViewState.value = Right(success); + } + void _getEmailContentSuccess(GetEmailContentSuccess success) { - if (success.attachments.isNotEmpty) { - initialAttachments = success.attachments; - uploadController.initializeUploadAttachments( - success.attachments.listAttachmentsDisplayedOutSide); - } + _initAttachments(success.attachments); emailContentsViewState.value = Right(success); - _emailContents = success.emailContents; } - String? getEmailContentDraftsAsHtml() => _emailContents?.asHtmlString; + void _transformHtmlEmailContent(String? emailContent) { + emailContentsViewState(Right(TransformHtmlEmailContentLoading())); + if (emailContent?.isEmpty == true) { + consumeState(Stream.value(Left(TransformHtmlEmailContentFailure(EmptyEmailContentException())))); + } else { + consumeState(_transformHtmlEmailContentInteractor.execute( + emailContent!, + TransformConfiguration.forReplyForwardEmail() + )); + } + } String getEmailAddressSender() { final arguments = composerArguments.value; if (arguments != null) { - if (arguments.emailActionType == EmailActionType.edit) { + if (arguments.emailActionType == EmailActionType.editDraft) { return arguments.presentationEmail?.from?.first.emailAddress ?? ''; } else { return mailboxDashBoardController.userProfile.value?.email ?? ''; @@ -1092,15 +1438,24 @@ class ComposerController extends BaseController { } void clearFocusEditor(BuildContext context) { - if (!kIsWeb) { + if (PlatformInfo.isMobile) { htmlEditorApi?.unfocus(); + KeyboardUtils.hideSystemKeyboardMobile(); } - FocusManager.instance.primaryFocus?.unfocus(); + FocusScope.of(context).unfocus(); } - void closeComposerWeb() { + void _closeComposerAction({dynamic result}) { + if (PlatformInfo.isWeb) { + _closeComposerWeb(result: result); + } else { + popBack(result: result); + } + } + + void _closeComposerWeb({dynamic result}) { FocusManager.instance.primaryFocus?.unfocus(); - mailboxDashBoardController.closeComposerOverlay(); + mailboxDashBoardController.closeComposerOverlay(result: result); } void displayScreenTypeComposerAction(ScreenDisplayMode displayMode) { @@ -1128,17 +1483,29 @@ class ComposerController extends BaseController { } void addEmailAddressType(PrefixEmailAddress prefixEmailAddress) { - listEmailAddressType.add(prefixEmailAddress); + switch(prefixEmailAddress) { + case PrefixEmailAddress.cc: + ccRecipientState.value = PrefixRecipientState.enabled; + break; + case PrefixEmailAddress.bcc: + bccRecipientState.value = PrefixRecipientState.enabled; + break; + default: + break; + } } void deleteEmailAddressType(PrefixEmailAddress prefixEmailAddress) { - listEmailAddressType.remove(prefixEmailAddress); updateListEmailAddress(prefixEmailAddress, []); switch(prefixEmailAddress) { case PrefixEmailAddress.cc: + ccRecipientState.value = PrefixRecipientState.disabled; + ccAddressFocusNode = FocusNode(); ccEmailAddressController.clear(); break; case PrefixEmailAddress.bcc: + bccRecipientState.value = PrefixRecipientState.disabled; + bccAddressFocusNode = FocusNode(); bccEmailAddressController.clear(); break; default: @@ -1146,17 +1513,7 @@ class ComposerController extends BaseController { } } - void onSubjectEmailFocusChange(bool isFocus) { - log('ComposerController::onSubjectEmailFocusChange(): Focus: $isFocus'); - if (isFocus) { - _collapseAllRecipient(); - htmlEditorApi?.unfocus(); - _autoCreateEmailTag(); - } - } - void onEditorFocusChange(bool isFocus) { - log('ComposerController::onEditorFocusChange(): Focus: $isFocus'); if (isFocus) { _collapseAllRecipient(); _autoCreateEmailTag(); @@ -1173,9 +1530,6 @@ class ComposerController extends BaseController { final inputToEmail = toEmailAddressController.text; final inputCcEmail = ccEmailAddressController.text; final inputBccEmail = bccEmailAddressController.text; - log('ComposerController::_autoCreateEmailTag():inputToEmail: $inputToEmail'); - log('ComposerController::_autoCreateEmailTag():inputCcEmail: $inputCcEmail'); - log('ComposerController::_autoCreateEmailTag():inputBccEmail: $inputBccEmail'); if (inputToEmail.isNotEmpty) { _autoCreateToEmailTag(inputToEmail); @@ -1288,27 +1642,23 @@ class ComposerController extends BaseController { break; } _closeSuggestionBox(); + if (PlatformInfo.isMobile) { + htmlEditorApi?.unfocus(); + } } else { - if (!BuildUtils.isWeb) { + if (PlatformInfo.isMobile) { _collapseAllRecipient(); _autoCreateEmailTag(); } } } - Future selectIdentity(Identity? newIdentity) async { - final formerIdentity = identitySelected.value; - identitySelected.value = newIdentity; + Future _selectIdentity(Identity? newIdentity) async { + final formerIdentity = identitySelected; + identitySelected = newIdentity; if (newIdentity != null) { await _applyIdentityForAllFieldComposer(formerIdentity, newIdentity); } - return Future.value(null); - } - - bool get _isMobileApp { - return !BuildUtils.isWeb - && currentContext != null - && _responsiveUtils.isMobile(currentContext!); } Future _applyIdentityForAllFieldComposer( @@ -1316,43 +1666,30 @@ class ComposerController extends BaseController { Identity newIdentity ) async { if (formerIdentity != null) { - // Remove former identity if (formerIdentity.bcc?.isNotEmpty == true) { _removeBccEmailAddressFromFormerIdentity(formerIdentity.bcc!); } - - if (!_isMobileApp) { - _removeSignature(); - } + await _removeSignature(); } - // Add new identity if (newIdentity.bcc?.isNotEmpty == true) { - await _applyBccEmailAddressFromIdentity(newIdentity.bcc!); + _applyBccEmailAddressFromIdentity(newIdentity.bcc!); } - if (!_isMobileApp) { - if (newIdentity.htmlSignature?.value.isNotEmpty == true) { - _applySignature(newIdentity.htmlSignature!); - } else if (newIdentity.textSignature?.value.isNotEmpty == true) { - _applySignature(newIdentity.textSignature!); - } + if (newIdentity.signatureAsString.isNotEmpty == true) { + await _applySignature(newIdentity.signatureAsString.asSignatureHtml()); } - - return Future.value(null); } - Future _applyBccEmailAddressFromIdentity(Set listEmailAddress) { - if (!listEmailAddressType.contains(PrefixEmailAddress.bcc)) { - listEmailAddressType.add(PrefixEmailAddress.bcc); + void _applyBccEmailAddressFromIdentity(Set listEmailAddress) { + if (bccRecipientState.value == PrefixRecipientState.disabled) { + bccRecipientState.value = PrefixRecipientState.enabled; } listBccEmailAddress = listEmailAddress.toList(); toAddressExpandMode.value = ExpandMode.COLLAPSE; ccAddressExpandMode.value = ExpandMode.COLLAPSE; bccAddressExpandMode.value = ExpandMode.COLLAPSE; _updateStatusEmailSendButton(); - - return Future.value(null); } void _removeBccEmailAddressFromFormerIdentity(Set listEmailAddress) { @@ -1360,7 +1697,7 @@ class ComposerController extends BaseController { .where((address) => !listEmailAddress.contains(address)) .toList(); if (listBccEmailAddress.isEmpty) { - listEmailAddressType.remove(PrefixEmailAddress.bcc); + bccRecipientState.value = PrefixRecipientState.disabled; } toAddressExpandMode.value = ExpandMode.COLLAPSE; ccAddressExpandMode.value = ExpandMode.COLLAPSE; @@ -1368,52 +1705,55 @@ class ComposerController extends BaseController { _updateStatusEmailSendButton(); } - void _applySignature(Signature signature) { - final signatureAsHtml = '--

${signature.value}'; - log('ComposerController::_applySignature(): $signatureAsHtml'); - if (BuildUtils.isWeb) { - richTextWebController.editorController.insertSignature(signatureAsHtml); + Future _applySignature(String signature) async { + if (PlatformInfo.isWeb) { + richTextWebController.editorController.insertSignature(signature); } else { - htmlEditorApi?.insertSignature(signatureAsHtml); + await htmlEditorApi?.insertSignature(signature); } } - void _removeSignature() { + Future _removeSignature() async { log('ComposerController::_removeSignature():'); - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { richTextWebController.editorController.removeSignature(); } else { - htmlEditorApi?.removeSignature(); + await htmlEditorApi?.removeSignature(); } } void insertImage(BuildContext context, double maxWith) async { + clearFocusEditor(context); + if (_responsiveUtils.isMobile(context)) { maxWithEditor = maxWith - 40; } else { maxWithEditor = maxWith - 120; } final inlineImage = await _selectFromFile(); - log('ComposerController::insertImage(): $inlineImage'); if (inlineImage != null) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { _insertImageOnWeb(inlineImage); } else { _insertImageOnMobileAndTablet(inlineImage); } + } else { + if (context.mounted) { + _appToast.showToastErrorMessage(context, AppLocalizations.of(context).cannotSelectThisImage); + } } } Future _selectFromFile() async { final filePickerResult = await FilePicker.platform.pickFiles( type: FileType.image, - withData: !BuildUtils.isWeb, - withReadStream: BuildUtils.isWeb); + withData: PlatformInfo.isMobile, + withReadStream: PlatformInfo.isWeb); final platformFile = filePickerResult?.files.single; if (platformFile != null) { final fileSelected = FileInfo( platformFile.name, - BuildUtils.isWeb ? '' : platformFile.path ?? '', + PlatformInfo.isWeb ? '' : platformFile.path ?? '', platformFile.size, bytes: platformFile.bytes, readStream: platformFile.readStream); @@ -1439,13 +1779,18 @@ class ComposerController extends BaseController { } } - void _uploadInlineAttachmentsAction(FileInfo pickedFile) async { + void _uploadInlineAttachmentsAction(FileInfo pickedFile, {bool fromFileShared = false}) async { if (uploadController.hasEnoughMaxAttachmentSize(listFiles: [pickedFile])) { final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { - final uploadUri = session.getUploadUri(accountId); - uploadController.uploadFileAction(pickedFile, uploadUri, isInline: true); + final uploadUri = session.getUploadUri(accountId, jmapUrl: _dynamicUrlInterceptors.jmapUrl); + uploadController.uploadFileAction( + pickedFile, + uploadUri, + isInline: true, + fromFileShared: fromFileShared + ); } } else { if (currentContext != null) { @@ -1464,26 +1809,84 @@ class ComposerController extends BaseController { void _handleUploadInlineSuccess(SuccessAttachmentUploadState uploadState) { uploadController.clearUploadInlineViewState(); - final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(); + final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl); final accountId = mailboxDashBoardController.accountId.value; if (baseDownloadUrl != null && accountId != null) { final imageUrl = uploadState.attachment.getDownloadUrl(baseDownloadUrl, accountId); log('ComposerController::_handleUploadInlineSuccess(): imageUrl: $imageUrl'); consumeState(_downloadImageAsBase64Interactor.execute( - imageUrl, - uploadState.attachment.cid!, - uploadState.fileInfo, - maxWidth: maxWithEditor)); + imageUrl, + uploadState.attachment.cid!, + uploadState.fileInfo, + maxWidth: maxWithEditor, + fromFileShared: uploadState.fromFileShared, + )); } } - void closeComposer() { - FocusManager.instance.primaryFocus?.unfocus(); - popBack(); + void closeComposer(BuildContext context) { + FocusScope.of(context).unfocus(); + + if (PlatformInfo.isWeb) { + mailboxDashBoardController.closeComposerOverlay(); + } else { + popBack(); + } } - void onEnterKeyDown() { + void _onEditorFocusOnMobile() { + if (Platform.isAndroid) { + _collapseAllRecipient(); + _autoCreateEmailTag(); + removeFocusAllInputEditorHeader(); + } + } + + void removeFocusAllInputEditorHeader() { + subjectEmailInputFocusNode?.unfocus(); + toAddressFocusNode?.unfocus(); + ccAddressFocusNode?.unfocus(); + bccAddressFocusNode?.unfocus(); + } + + void _onChangeCursorOnMobile(List? coordinates, BuildContext context) { + final headerEditorMobileWidgetRenderObject = headerEditorMobileWidgetKey.currentContext?.findRenderObject(); + if (headerEditorMobileWidgetRenderObject is RenderBox?) { + final headerEditorMobileSize = headerEditorMobileWidgetRenderObject?.size; + if (coordinates?[1] != null && coordinates?[1] != 0) { + final coordinateY = max((coordinates?[1] ?? 0) - defaultPaddingCoordinateYCursorEditor, 0); + final realCoordinateY = coordinateY + (headerEditorMobileSize?.height ?? 0); + final outsideHeight = Get.height - ComposerStyle.keyboardMaxHeight - ComposerStyle.keyboardToolBarHeight; + final webViewEditorClientY = max(outsideHeight, 0) + scrollController.position.pixels; + if (scrollController.position.pixels >= realCoordinateY) { + _scrollToCursorEditor( + realCoordinateY.toDouble(), + headerEditorMobileSize?.height ?? 0, + context, + ); + } else if ((realCoordinateY) >= webViewEditorClientY) { + _scrollToCursorEditor( + realCoordinateY.toDouble(), + headerEditorMobileSize?.height ?? 0, + context, + ); + } + } + } + } + + void _scrollToCursorEditor( + double realCoordinateY, + double headerEditorMobileHeight, + BuildContext context, + ) { + scrollController.jumpTo( + realCoordinateY - (_responsiveUtils.isLandscapeMobile(context) ? 0 : headerEditorMobileHeight / 2), + ); + } + + void _onEnterKeyDown() { if(scrollController.position.pixels < scrollController.position.maxScrollExtent) { scrollController.animateTo( scrollController.position.pixels + 20, @@ -1504,11 +1907,200 @@ class ComposerController extends BaseController { toAddressFocusNode?.requestFocus(); } else if (subjectEmailInputController.text.isEmpty) { subjectEmailInputFocusNode?.requestFocus(); - } else if (BuildUtils.isWeb) { + } else if (PlatformInfo.isWeb) { Future.delayed( const Duration(milliseconds: 500), richTextWebController.editorController.setFocus ); } } + + void handleInitHtmlEditorWeb(String initContent) { + richTextWebController.editorController.setFullScreen(); + onChangeTextEditorWeb(initContent); + richTextWebController.setEnableCodeView(); + if (identitySelected == null) { + _getAllIdentities(); + } + } + + void handleOnFocusHtmlEditorWeb() { + FocusManager.instance.primaryFocus?.unfocus(); + Future.delayed(const Duration(milliseconds: 500), () { + richTextWebController.editorController.setFocus(); + }); + richTextWebController.closeAllMenuPopup(); + } + + void handleOnUnFocusHtmlEditorWeb() { + onEditorFocusChange(false); + } + + void handleOnMouseDownHtmlEditorWeb(BuildContext context) { + Navigator.maybePop(context); + FocusScope.of(context).unfocus(); + onEditorFocusChange(true); + } + + void handleImageUploadSuccess ( + BuildContext context, + web_html_editor.FileUpload fileUpload + ) async { + log('ComposerController::handleImageUploadSuccess:NAME: ${fileUpload.name} | TYPE: ${fileUpload.type} | SIZE: ${fileUpload.size}'); + if (fileUpload.base64 == null) { + _appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).can_not_upload_this_file_as_attachments + ); + return; + } + + if (fileUpload.type?.startsWith(MediaTypeExtension.imageType) == true) { + _addInlineImageFromDragAndDrop( + base64Data: fileUpload.base64!, + name: fileUpload.name, + ); + } else { + final fileInfo = await fileUpload.toFileInfo(); + if (fileInfo != null) { + _addAttachmentFromDragAndDrop(fileInfo: fileInfo); + } else if (context.mounted) { + _appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).can_not_upload_this_file_as_attachments + ); + } + } + } + + void _addInlineImageFromDragAndDrop({required String base64Data, String? name}) { + log('ComposerController::_addInlineImageFromDragAndDrop:name: $name'); + richTextWebController.insertInlineImage(base64Data: base64Data, name: name); + } + + void handleImageUploadFailure({ + required BuildContext context, + required web_html_editor.UploadError uploadError, + web_html_editor.FileUpload? fileUpload, + String? base64Str, + }) { + logError('ComposerController::handleImageUploadFailure:fileUpload: $fileUpload | uploadError: $uploadError'); + _appToast.showToastErrorMessage( + context, + '${AppLocalizations.of(context).can_not_upload_this_file_as_attachments}. (${uploadError.name})' + ); + } + + void _addAttachmentFromDragAndDrop({required FileInfo fileInfo}) { + if (uploadController.hasEnoughMaxAttachmentSize(listFiles: [fileInfo])) { + _uploadAttachmentsAction([fileInfo]); + } else { + if (currentContext != null) { + showConfirmDialogAction( + currentContext!, + AppLocalizations.of(currentContext!).message_dialog_upload_attachments_exceeds_maximum_size( + filesize(mailboxDashBoardController.maxSizeAttachmentsPerEmail?.value ?? 0, 0)), + AppLocalizations.of(currentContext!).got_it, + title: AppLocalizations.of(currentContext!).maximum_files_size, + hasCancelButton: false, + ); + } + } + } + + FocusNode? getNextFocusOfToEmailAddress() { + if (ccRecipientState.value == PrefixRecipientState.enabled) { + return ccAddressFocusNode; + } else if (bccRecipientState.value == PrefixRecipientState.enabled) { + return bccAddressFocusNode; + } else { + return subjectEmailInputFocusNode; + } + } + + FocusNode? getNextFocusOfCcEmailAddress() { + if (bccRecipientState.value == PrefixRecipientState.enabled) { + return bccAddressFocusNode; + } else { + return subjectEmailInputFocusNode; + } + } + + void handleFocusNextAddressAction() { + _autoCreateEmailTag(); + } + + bool get isNetworkConnectionAvailable => networkConnectionController.isNetworkConnectionAvailable(); + + UserProfile? get userProfile => mailboxDashBoardController.userProfile.value; + + String? get textEditorWeb => _textEditorWeb; + + HtmlEditorApi? get htmlEditorApi => richTextMobileTabletController.htmlEditorApi; + + void onChangeTextEditorWeb(String? text) { + initTextEditor(text); + _textEditorWeb = text; + } + + void initTextEditor(String? text) { + _initTextEditor ??= text; + } + + void setSubjectEmail(String subject) => subjectEmail.value = subject; + + Future _getEmailBodyText(BuildContext context, {bool asDrafts = false}) async { + var contentHtml = ''; + + if (PlatformInfo.isWeb) { + if (_responsiveUtils.isDesktop(context) && + screenDisplayMode.value == ScreenDisplayMode.minimize) { + contentHtml = _textEditorWeb ?? ''; + } else { + if (asDrafts) { + contentHtml = await richTextWebController.editorController.getText(); + } else { + contentHtml = await richTextWebController.editorController.getTextWithSignatureContent(); + } + } + } else { + if (asDrafts) { + contentHtml = (await htmlEditorApi?.getText()) ?? ''; + } else { + contentHtml = (await htmlEditorApi?.getTextWithSignatureContent()) ?? ''; + } + } + + final newContentHtml = contentHtml.removeEditorStartTag(); + return newContentHtml; + } + + void removeDraggableEmailAddress(DraggableEmailAddress draggableEmailAddress) { + log('ComposerController::removeDraggableEmailAddress: $draggableEmailAddress'); + switch(draggableEmailAddress.prefix) { + case PrefixEmailAddress.to: + listToEmailAddress.remove(draggableEmailAddress.emailAddress); + toAddressExpandMode.value = ExpandMode.EXPAND; + break; + case PrefixEmailAddress.cc: + listCcEmailAddress.remove(draggableEmailAddress.emailAddress); + ccAddressExpandMode.value = ExpandMode.EXPAND; + break; + case PrefixEmailAddress.bcc: + listBccEmailAddress.remove(draggableEmailAddress.emailAddress); + bccAddressExpandMode.value = ExpandMode.EXPAND; + break; + default: + break; + } + isInitialRecipient.value = true; + isInitialRecipient.refresh(); + + _updateStatusEmailSendButton(); + } + + void addAttachmentFromDropZone(Attachment attachment) { + log('ComposerController::addAttachmentFromDropZone: $attachment'); + uploadController.initializeUploadAttachments([attachment]); + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_view.dart b/lib/features/composer/presentation/composer_view.dart index 081419030e..82bd999d2c 100644 --- a/lib/features/composer/presentation/composer_view.dart +++ b/lib/features/composer/presentation/composer_view.dart @@ -1,248 +1,369 @@ -import 'package:core/core.dart'; -import 'package:dropdown_button2/dropdown_button2.dart'; -import 'package:enough_html_editor/enough_html_editor.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/context_menu/simple_context_menu_action_builder.dart'; +import 'package:core/presentation/views/responsive/responsive_widget.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:filesize/filesize.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:jmap_dart_client/jmap/identities/identity.dart'; -import 'package:model/model.dart'; -import 'package:rich_text_composer/rich_text_composer.dart' as rich_text_composer; -import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart'; -import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:model/email/prefix_email_address.dart'; +import 'package:tmail_ui_user/features/base/widget/popup_item_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; -import 'package:tmail_ui_user/features/composer/presentation/mixin/composer_loading_mixin.dart'; -import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_file_composer_builder.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/email_address_input_builder.dart'; -import 'package:tmail_ui_user/features/upload/presentation/extensions/list_upload_file_state_extension.dart'; -import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/mobile_app_bar_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/mobile/mobile_container_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/mobile/mobile_editor_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/mobile/tablet_container_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/desktop_app_bar_composer_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -class ComposerView extends GetWidget - with AppLoaderMixin, RichTextButtonMixin, ComposerLoadingMixin { +class ComposerView extends GetWidget { - final responsiveUtils = Get.find(); - final imagePaths = Get.find(); - final keyboardUtils = Get.find(); + final _responsiveUtils = Get.find(); + final _imagePaths = Get.find(); ComposerView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ResponsiveWidget( - responsiveUtils: responsiveUtils, - mobile: _buildComposerViewForMobile(context), - tablet: _buildComposerViewForTablet(context), - ); - } - - Widget _buildComposerViewForMobile(BuildContext context) { - return WillPopScope( - onWillPop: () async { - controller.saveEmailAsDrafts(context, canPop: false); - return true; - }, - child: LayoutBuilder( - builder: (context, constraints) => GestureDetector( - onTap: () { - controller.clearFocusEditor(context); - }, - child: Scaffold( - backgroundColor: Colors.white, - body: SafeArea( - right: responsiveUtils.isLandscapeMobile(context), - left: responsiveUtils.isLandscapeMobile(context), - child: Container( - color: Colors.white, - child: Column(children: [ - Obx(() => _buildAppBar(context, controller.isEnableEmailSendButton.value)), - const Divider(color: AppColor.colorDividerComposer, height: 1), - Expanded(child: _buildBodyMobile(context, constraints.maxWidth)) - ]) - ), - ), - ) - ), - ) - ); - } - - Widget _buildComposerViewForTablet(BuildContext context) { - return WillPopScope( - onWillPop: () async { - controller.saveEmailAsDrafts(context, canPop: false); - return true; - }, - child: GestureDetector( - onTap: () { - controller.clearFocusEditor(context); - }, - child: Scaffold( - backgroundColor: Colors.black38, - body: LayoutBuilder(builder: (context, constraints) { - return rich_text_composer.KeyboardRichText( - richTextController: controller.keyboardRichTextController, - keyBroadToolbar: RichTextKeyboardToolBar( - backgroundKeyboardToolBarColor: AppColor.colorBackgroundKeyboard, - insertAttachment: () => controller.openPickAttachmentMenu(context, _pickAttachmentsActionTiles(context)), - insertImage: () => controller.insertImage(context, constraints.maxWidth), - richTextController: controller.keyboardRichTextController, - titleQuickStyleBottomSheet: AppLocalizations.of(context).titleQuickStyles, - titleBackgroundBottomSheet: AppLocalizations.of(context).titleBackground, - titleForegroundBottomSheet: AppLocalizations.of(context).titleForeground, - titleFormatBottomSheet: AppLocalizations.of(context).titleFormat, - titleBack: AppLocalizations.of(context).format, - ), - child: Align(alignment: Alignment.center, child: Card( - color: Colors.transparent, - elevation: 20, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - shadowColor: Colors.transparent, - child: Container( - decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(24))), - width: responsiveUtils.getSizeScreenWidth(context) * 0.9, - height: responsiveUtils.getSizeScreenHeight(context) * 0.9, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(24)), - child: SafeArea( - child: Column(children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: _buildAppBar(context, controller.isEnableEmailSendButton.value)), - const Padding(padding: EdgeInsets.only(top: 8), child: Divider(color: AppColor.colorDividerComposer, height: 1)), - Expanded(child: _buildBodyTablet(context)), - const Divider(color: AppColor.colorDividerComposer, height: 1), - Obx(() => _buildBottomBar(context, controller.isEnableEmailSendButton.value)), - ]), - ) - ) - ) - )), - ); - }) + responsiveUtils: _responsiveUtils, + mobile: MobileContainerView( + keyboardRichTextController: controller.keyboardRichTextController, + onCloseViewAction: () => controller.saveToDraftAndClose(context, canPop: false), + onClearFocusAction: () => controller.clearFocusEditor(context), + onAttachFileAction: () => controller.isNetworkConnectionAvailable + ? controller.openPickAttachmentMenu( + context, + _pickAttachmentsActionTiles(context) ) - ) - ); - } - - Widget _buildAppBar(BuildContext context, bool isEnableSendButton) { - return Container( - padding: responsiveUtils.isMobile(context) && responsiveUtils.isLandscapeMobile(context) - ? const EdgeInsets.all(8) - : EdgeInsets.zero, - color: Colors.white, - child: Row( - children: [ - buildIconWeb( - icon: SvgPicture.asset(imagePaths.icClose, width: 30, height: 30, fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).close, - iconPadding: EdgeInsets.zero, - onTap: () => controller.saveEmailAsDrafts(context)), - Expanded(child: _buildTitleComposer(context)), - if (responsiveUtils.isScreenWithShortestSide(context)) - buildIconWeb( - icon: SvgPicture.asset( - isEnableSendButton ? imagePaths.icSendMobile : imagePaths.icSendDisable, - fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).send, - onTap: () => controller.sendEmailAction(context)), - if (responsiveUtils.isScreenWithShortestSide(context)) - buildIconWithLowerMenu( - SvgPicture.asset(imagePaths.icRequestReadReceipt), - context, - _popUpMoreActionMenu(context), - controller.openPopupMenuAction), - ], + : null, + onInsertImageAction: (constraints) => controller.isNetworkConnectionAvailable + ? controller.insertImage(context, constraints.maxWidth) + : null, + backgroundColor: MobileAppBarComposerWidgetStyle.backgroundColor, + childBuilder: (context) => SafeArea( + left: !_responsiveUtils.isLandscapeMobile(context), + right: !_responsiveUtils.isLandscapeMobile(context), + child: Container( + color: ComposerStyle.mobileBackgroundColor, + child: Column( + children: [ + if (_responsiveUtils.isLandscapeMobile(context)) + Obx(() => LandscapeAppBarComposerWidget( + isSendButtonEnabled: controller.isEnableEmailSendButton.value, + onCloseViewAction: () => controller.saveToDraftAndClose(context), + sendMessageAction: () => controller.sendEmailAction(context), + openContextMenuAction: (position) { + controller.openPopupMenuAction( + context, + position, + _createMoreOptionPopupItems(context), + radius: ComposerStyle.popupMenuRadius + ); + }, + )) + else + Obx(() => AppBarComposerWidget( + isSendButtonEnabled: controller.isEnableEmailSendButton.value, + onCloseViewAction: () => controller.saveToDraftAndClose(context), + sendMessageAction: () => controller.sendEmailAction(context), + openContextMenuAction: (position) { + controller.openPopupMenuAction( + context, + position, + _createMoreOptionPopupItems(context), + radius: ComposerStyle.popupMenuRadius + ); + }, + )), + Expanded( + child: SafeArea( + top: false, + bottom: false, + child: SingleChildScrollView( + controller: controller.scrollController, + physics: const ClampingScrollPhysics(), + child: Column( + children: [ + Obx(() => Column( + children: [ + RecipientComposerWidget( + prefix: PrefixEmailAddress.to, + listEmailAddress: controller.listToEmailAddress, + ccState: controller.ccRecipientState.value, + bccState: controller.bccRecipientState.value, + expandMode: controller.toAddressExpandMode.value, + controller: controller.toEmailAddressController, + focusNode: controller.toAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyToEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + padding: ComposerStyle.mobileRecipientPadding, + margin: ComposerStyle.mobileRecipientMargin, + nextFocusNode: controller.getNextFocusOfToEmailAddress(), + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onAddEmailAddressTypeAction: controller.addEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + ), + if (controller.ccRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.cc, + listEmailAddress: controller.listCcEmailAddress, + expandMode: controller.ccAddressExpandMode.value, + controller: controller.ccEmailAddressController, + focusNode: controller.ccAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyCcEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.getNextFocusOfCcEmailAddress(), + padding: ComposerStyle.mobileRecipientPadding, + margin: ComposerStyle.mobileRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + ), + if (controller.bccRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.bcc, + listEmailAddress: controller.listBccEmailAddress, + expandMode: controller.bccAddressExpandMode.value, + controller: controller.bccEmailAddressController, + focusNode: controller.bccAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyBccEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.subjectEmailInputFocusNode, + padding: ComposerStyle.mobileRecipientPadding, + margin: ComposerStyle.mobileRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + ), + ], + )), + SubjectComposerWidget( + focusNode: controller.subjectEmailInputFocusNode, + textController: controller.subjectEmailInputController, + onTextChange: controller.setSubjectEmail, + padding: ComposerStyle.mobileSubjectPadding, + margin: ComposerStyle.mobileSubjectMargin, + ), + Obx(() => Center( + child: InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + ), + )), + Obx(() => GestureDetector( + onTapDown: (_) { + controller.removeFocusAllInputEditorHeader(); + }, + child: Padding( + padding: ComposerStyle.mobileEditorPadding, + child: MobileEditorView( + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + onCreatedEditorAction: controller.onCreatedMobileEditorAction, + onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction, + ), + ), + )), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return MobileAttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + ); + } else { + return const SizedBox.shrink(); + } + }), + const SizedBox(height: ComposerStyle.keyboardMaxHeight), + ], + ), + ), + ) + ) + ]) + ), + ), ), - ); - } - - List _popUpMoreActionMenu(BuildContext context) { - return [ - PopupMenuItem( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - children: [ - Obx(() => buildIconWeb( - icon: Icon(controller.hasRequestReadReceipt.value ? Icons.done : null, color: Colors.black))), - IgnorePointer( - child: buildTextIcon( - AppLocalizations.of(context).requestReadReceipt, - textStyle: const TextStyle(color: Colors.black, fontSize: 15)), - ), - ]), - onTap: () { - controller.toggleRequestReadReceipt(); - }, - ) - ]; - } - - Widget _buildBottomBar(BuildContext context, bool isEnableSendButton) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 16), - color: Colors.white, - child: Stack( - alignment: Alignment.centerRight, - children: [ - Row(mainAxisAlignment: MainAxisAlignment.center, + tablet: TabletContainerView( + keyboardRichTextController: controller.keyboardRichTextController, + onCloseViewAction: () => controller.saveToDraftAndClose(context, canPop: false), + onClearFocusAction: () => controller.clearFocusEditor(context), + onAttachFileAction: () => controller.isNetworkConnectionAvailable + ? controller.openPickAttachmentMenu( + context, + _pickAttachmentsActionTiles(context) + ) + : null, + onInsertImageAction: (constraints) => controller.isNetworkConnectionAvailable + ? controller.insertImage(context, constraints.maxWidth) + : null, + childBuilder: (context, constraints) => Container( + color: ComposerStyle.mobileBackgroundColor, + child: Column( children: [ - buildTextButton( - AppLocalizations.of(context).cancel, - textStyle: const TextStyle(fontWeight: FontWeight.w500, fontSize: 17, color: AppColor.lineItemListColor), - backgroundColor: AppColor.emailAddressChipColor, - width: 150, - height: 44, - radius: 10, - onTap: () => controller.closeComposer()), - const SizedBox(width: 12), - buildTextButton( - AppLocalizations.of(context).save_to_drafts, - textStyle: const TextStyle(fontWeight: FontWeight.w500, fontSize: 17, color: AppColor.colorTextButton), - backgroundColor: AppColor.emailAddressChipColor, - width: 150, - height: 44, - radius: 10, - onTap: () => controller.saveEmailAsDrafts(context)), - const SizedBox(width: 12), - buildTextButton( - AppLocalizations.of(context).send, - width: 150, - height: 44, - radius: 10, - onTap: () => controller.sendEmailAction(context)), + Obx(() => DesktopAppBarComposerWidget( + emailSubject: controller.subjectEmail.value ?? '', + onCloseViewAction: () => controller.saveToDraftAndClose(context), + constraints: constraints, + )), + Expanded( + child: SingleChildScrollView( + controller: controller.scrollController, + physics: const ClampingScrollPhysics(), + child: Column( + children: [ + Obx(() => Column( + children: [ + RecipientComposerWidget( + prefix: PrefixEmailAddress.to, + listEmailAddress: controller.listToEmailAddress, + ccState: controller.ccRecipientState.value, + bccState: controller.bccRecipientState.value, + expandMode: controller.toAddressExpandMode.value, + controller: controller.toEmailAddressController, + focusNode: controller.toAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyToEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + padding: ComposerStyle.mobileRecipientPadding, + margin: ComposerStyle.mobileRecipientMargin, + nextFocusNode: controller.getNextFocusOfToEmailAddress(), + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onAddEmailAddressTypeAction: controller.addEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + ), + if (controller.ccRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.cc, + listEmailAddress: controller.listCcEmailAddress, + expandMode: controller.ccAddressExpandMode.value, + controller: controller.ccEmailAddressController, + focusNode: controller.ccAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyCcEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.getNextFocusOfCcEmailAddress(), + padding: ComposerStyle.mobileRecipientPadding, + margin: ComposerStyle.mobileRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + ), + if (controller.bccRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.bcc, + listEmailAddress: controller.listBccEmailAddress, + expandMode: controller.bccAddressExpandMode.value, + controller: controller.bccEmailAddressController, + focusNode: controller.bccAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyBccEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.subjectEmailInputFocusNode, + padding: ComposerStyle.mobileRecipientPadding, + margin: ComposerStyle.mobileRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + ), + ], + )), + SubjectComposerWidget( + focusNode: controller.subjectEmailInputFocusNode, + textController: controller.subjectEmailInputController, + onTextChange: controller.setSubjectEmail, + padding: ComposerStyle.mobileSubjectPadding, + margin: ComposerStyle.mobileSubjectMargin, + ), + Obx(() => Center( + child: InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + ), + )), + Obx(() => GestureDetector( + onTapDown: (_) { + controller.removeFocusAllInputEditorHeader(); + }, + child: Padding( + padding: ComposerStyle.mobileEditorPadding, + child: MobileEditorView( + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + onCreatedEditorAction: controller.onCreatedMobileEditorAction, + onLoadCompletedEditorAction: controller.onLoadCompletedMobileEditorAction, + ), + ), + )), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return MobileAttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + ); + } else { + return const SizedBox.shrink(); + } + }) + ], + ), + ) + ), + TabletBottomBarComposerWidget( + deleteComposerAction: () => controller.closeComposer(context), + saveToDraftAction: () => controller.saveToDraftAction(context), + sendMessageAction: () => controller.sendEmailAction(context), + requestReadReceiptAction: (position) { + controller.openPopupMenuAction( + context, + position, + _createReadReceiptPopupItems(context), + radius: ComposerStyle.popupMenuRadius + ); + }, + ), ] - ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - buildIconWithUpperMenu( - SvgPicture.asset(imagePaths.icRequestReadReceipt), - context, - _popUpMoreActionMenu(context), - controller.openPopupMenuAction) - ]), - ], + ) + ), ), ); } - Widget _buildTitleComposer(BuildContext context) { - return Obx(() => Text( - controller.subjectEmail.isNotEmpty == true - ? controller.subjectEmail.value ?? '' - : AppLocalizations.of(context).new_message.capitalizeFirstEach, - overflow: TextOverflow.ellipsis, - maxLines: 1, - textAlign: TextAlign.center, - style: TextStyle(fontSize: responsiveUtils.isMobile(context) ? 17 : 24, fontWeight: FontWeight.bold, color: Colors.black), - )); - } - List _pickAttachmentsActionTiles(BuildContext context) { return [ _pickPhotoAndVideoAction(context), @@ -254,7 +375,7 @@ class ComposerView extends GetWidget Widget _pickPhotoAndVideoAction(BuildContext context) { return (SimpleContextMenuActionBuilder( const Key('pick_photo_and_video_context_menu_action'), - SvgPicture.asset(imagePaths.icPhotoLibrary, width: 24, height: 24, fit: BoxFit.fill), + SvgPicture.asset(_imagePaths.icPhotoLibrary, width: 24, height: 24, fit: BoxFit.fill), AppLocalizations.of(context).photos_and_videos) ..onActionClick((_) => controller.openFilePickerByType(context, FileType.media))) .build(); @@ -263,449 +384,77 @@ class ComposerView extends GetWidget Widget _browseFileAction(BuildContext context) { return (SimpleContextMenuActionBuilder( const Key('browse_file_context_menu_action'), - SvgPicture.asset(imagePaths.icMore, width: 24, height: 24, fit: BoxFit.fill), + SvgPicture.asset(_imagePaths.icMore, width: 24, height: 24, fit: BoxFit.fill), AppLocalizations.of(context).browse) ..onActionClick((_) => controller.openFilePickerByType(context, FileType.any))) .build(); } - Widget _buildFromEmailAddress(BuildContext context) { - return Padding( - padding: EdgeInsets.only( - left: responsiveUtils.isMobile(context) ? 16 : 0, - top: 12, - bottom: 12), - child: Row(children: [ - Text('${AppLocalizations.of(context).from_email_address_prefix}:', - style: const TextStyle(fontSize: 15, color: AppColor.colorHintEmailAddressInput)), - const SizedBox(width: 12), - DropdownButtonHideUnderline( - child: DropdownButton2( - isExpanded: true, - customButton: SvgPicture.asset(imagePaths.icEditIdentity), - items: controller.listIdentities.map((item) => DropdownMenuItem( - value: item, - child: Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: item == controller.identitySelected.value ? AppColor.colorBgMenuItemDropDownSelected : Colors.transparent), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - item.name ?? '', - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.normal, color: Colors.black), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - Text( - item.email ?? '', - style: const TextStyle(fontSize: 13, fontWeight: FontWeight.normal, color: AppColor.colorHintSearchBar), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ) - ] - ), - ), - )).toList(), - onChanged: (newIdentity) => controller.selectIdentity(newIdentity), - itemPadding: const EdgeInsets.symmetric(horizontal: 8), - customItemsHeight: 55, - dropdownMaxHeight: 240, - dropdownWidth: 300, - dropdownDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: Colors.white), - dropdownElevation: 4, - scrollbarRadius: const Radius.circular(40), - scrollbarThickness: 6, - ), - ), - Expanded(child: Padding( - padding: const EdgeInsets.only(right: 8, left: 12), - child: Text( - controller.identitySelected.value?.email ?? '', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 17, fontWeight: FontWeight.normal, color: AppColor.colorEmailAddressPrefix), - ))), - ]), - ); - } - - Widget _buildEmailAddress(BuildContext context) { - return Focus( - onFocusChange: (focus) { - if(focus) { - controller.htmlEditorApi?.unfocus(); - controller.keyboardRichTextController.hideRichTextView(); - } - }, - child: Column( - children: [ - Obx(() => Padding( - padding: EdgeInsets.only(left: responsiveUtils.isMobile(context) ? 16 : 0), - child: (EmailAddressInputBuilder(context, imagePaths, - PrefixEmailAddress.to, - controller.listToEmailAddress, - controller.listEmailAddressType, - expandMode: controller.toAddressExpandMode.value, - controller: controller.toEmailAddressController, - focusNode: controller.toAddressFocusNode, - autoDisposeFocusNode: false, - keyTagEditor: controller.keyToEmailTagEditor, - isInitial: controller.isInitialRecipient.value) - ..addOnFocusEmailAddressChangeAction((prefixEmailAddress, focus) => controller.onEmailAddressFocusChange(prefixEmailAddress, focus)) - ..addOnShowFullListEmailAddressAction((prefixEmailAddress) => controller.showFullEmailAddress(prefixEmailAddress)) - ..addOnAddEmailAddressTypeAction((prefixEmailAddress) => controller.addEmailAddressType(prefixEmailAddress)) - ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) - ..addOnSuggestionEmailAddress((word) => controller.getAutoCompleteSuggestion(word))) - .build() - )), - Obx(() => controller.listEmailAddressType.contains(PrefixEmailAddress.cc) == true - ? const Divider(color: AppColor.colorDividerComposer, height: 1) - : const SizedBox.shrink()), - Obx(() => controller.listEmailAddressType.contains(PrefixEmailAddress.cc) == true - ? Padding( - padding: EdgeInsets.only(left: responsiveUtils.isMobile(context) ? 16 : 0), - child: (EmailAddressInputBuilder(context, imagePaths, - PrefixEmailAddress.cc, - controller.listCcEmailAddress, - controller.listEmailAddressType, - expandMode: controller.ccAddressExpandMode.value, - controller: controller.ccEmailAddressController, - keyTagEditor: controller.keyCcEmailTagEditor, - isInitial: controller.isInitialRecipient.value,) - ..addOnFocusEmailAddressChangeAction((prefixEmailAddress, focus) => controller.onEmailAddressFocusChange(prefixEmailAddress, focus)) - ..addOnShowFullListEmailAddressAction((prefixEmailAddress) => controller.showFullEmailAddress(prefixEmailAddress)) - ..addOnDeleteEmailAddressTypeAction((prefixEmailAddress) => controller.deleteEmailAddressType(prefixEmailAddress)) - ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) - ..addOnSuggestionEmailAddress((word) => controller.getAutoCompleteSuggestion(word))) - .build()) - : const SizedBox.shrink() - ), - Obx(() => controller.listEmailAddressType.contains(PrefixEmailAddress.bcc) == true - ? const Divider(color: AppColor.colorDividerComposer, height: 1) - : const SizedBox.shrink()), - Obx(() => controller.listEmailAddressType.contains(PrefixEmailAddress.bcc) == true - ? Padding( - padding: EdgeInsets.only(left: responsiveUtils.isMobile(context) ? 16 : 0), - child: (EmailAddressInputBuilder(context, imagePaths, - PrefixEmailAddress.bcc, - controller.listBccEmailAddress, - controller.listEmailAddressType, - expandMode: controller.bccAddressExpandMode.value, - controller: controller.bccEmailAddressController, - keyTagEditor: controller.keyBccEmailTagEditor, - isInitial: controller.isInitialRecipient.value,) - ..addOnFocusEmailAddressChangeAction((prefixEmailAddress, focus) => controller.onEmailAddressFocusChange(prefixEmailAddress, focus)) - ..addOnShowFullListEmailAddressAction((prefixEmailAddress) => controller.showFullEmailAddress(prefixEmailAddress)) - ..addOnDeleteEmailAddressTypeAction((prefixEmailAddress) => controller.deleteEmailAddressType(prefixEmailAddress)) - ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) - ..addOnSuggestionEmailAddress((word) => controller.getAutoCompleteSuggestion(word))) - .build()) - : const SizedBox.shrink() - ), - ], - ), - ); - } - - Widget _buildSubjectEmail(BuildContext context) { - return Focus( - onFocusChange: (focus) { - if(focus) { - controller.htmlEditorApi?.unfocus(); - controller.keyboardRichTextController.hideRichTextView(); - } - }, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 8), - child: Text( - '${AppLocalizations.of(context).subject_email}:', - style: const TextStyle(fontSize: 15, color: AppColor.colorHintEmailAddressInput))), - Expanded( - child: FocusScope(child: Focus( - onFocusChange: (focus) => controller.onSubjectEmailFocusChange(focus), - child: (TextFieldBuilder() - ..key(const Key('subject_email_input')) - ..cursorColor(AppColor.colorTextButton) - ..maxLines(responsiveUtils.isMobile(context) ? null : 1) - ..addFocusNode(controller.subjectEmailInputFocusNode) - ..onChange((value) => controller.setSubjectEmail(value)) - ..textStyle(const TextStyle(color: Colors.black, fontSize: 15, fontWeight: FontWeight.normal)) - ..textDecoration(const InputDecoration(contentPadding: EdgeInsets.zero, border: InputBorder.none)) - ..addController(controller.subjectEmailInputController)) - .build(), - )) - ) - ] + List _createReadReceiptPopupItems(BuildContext context) { + return [ + PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemWidget( + _imagePaths.icReadReceipt, + AppLocalizations.of(context).requestReadReceipt, + styleName: ComposerStyle.popupItemTextStyle, + padding: ComposerStyle.popupItemPadding, + selectedIcon: _imagePaths.icFilterSelected, + isSelected: controller.hasRequestReadReceipt.value, + onCallbackAction: () { + popBack(); + controller.toggleRequestReadReceipt(); + } + ) ), - ); + ]; } - Widget _buildBodyMobile(BuildContext context, double maxWidth) { - return rich_text_composer.KeyboardRichText( - child: SingleChildScrollView( - controller: controller.scrollController, - physics: const ClampingScrollPhysics(), - child: Column(children: [ - Obx(() => controller.identitySelected.value != null - ? _buildFromEmailAddress(context) - : const SizedBox.shrink()), - Obx(() => controller.identitySelected.value != null - ? const Divider(color: AppColor.colorDividerComposer, height: 1) - : const SizedBox.shrink()), - _buildEmailAddress(context), - const Divider(color: AppColor.colorDividerComposer, height: 1), - Padding(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: _buildSubjectEmail(context)), - const Divider(color: AppColor.colorDividerComposer, height: 1), - _buildAttachmentsWidget(context), - buildInlineLoadingView(controller), - _buildComposerEditor(context), - ]) - ), - richTextController: controller.keyboardRichTextController, - keyBroadToolbar: RichTextKeyboardToolBar( - backgroundKeyboardToolBarColor: AppColor.colorBackgroundKeyboard, - isLandScapeMode: responsiveUtils.isLandscapeMobile(context), - insertAttachment: () => controller.openPickAttachmentMenu(context, _pickAttachmentsActionTiles(context)), - insertImage: () => controller.insertImage(context, maxWidth), - richTextController: controller.keyboardRichTextController, - titleQuickStyleBottomSheet: AppLocalizations.of(context).titleQuickStyles, - titleBackgroundBottomSheet: AppLocalizations.of(context).titleBackground, - titleForegroundBottomSheet: AppLocalizations.of(context).titleForeground, - titleFormatBottomSheet: AppLocalizations.of(context).titleFormat, - titleBack: AppLocalizations.of(context).format, + List _createMoreOptionPopupItems(BuildContext context) { + return [ + PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemWidget( + _imagePaths.icReadReceipt, + AppLocalizations.of(context).requestReadReceipt, + styleName: ComposerStyle.popupItemTextStyle, + padding: ComposerStyle.popupItemPadding, + colorIcon: ComposerStyle.popupItemIconColor, + selectedIcon: _imagePaths.icFilterSelected, + isSelected: controller.hasRequestReadReceipt.value, + onCallbackAction: () { + popBack(); + controller.toggleRequestReadReceipt(); + } + ) ), - ); - } - - Widget _buildBodyTablet(BuildContext context) { - return SingleChildScrollView( - controller: controller.scrollController, - physics: const ClampingScrollPhysics(), - child: Column(children: [ - Padding( - padding: const EdgeInsets.only(left: 16), - child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding(padding: const EdgeInsets.only(top: 20), - child: (AvatarBuilder() - ..text(controller.mailboxDashBoardController.userProfile.value?.getAvatarText() ?? '') - ..size(56) - ..addTextStyle(const TextStyle(fontWeight: FontWeight.w600, fontSize: 28, color: Colors.white)) - ..backgroundColor(AppColor.colorAvatar)) - .build()), - Expanded(child: Padding( - padding: const EdgeInsets.only(left: 16), - child: Column(children: [ - Obx(() => controller.identitySelected.value != null - ? _buildFromEmailAddress(context) - : const SizedBox.shrink()), - Obx(() => controller.identitySelected.value != null - ? const Divider(color: AppColor.colorDividerComposer, height: 1) - : const SizedBox.shrink()), - _buildEmailAddress(context), - const Divider(color: AppColor.colorDividerComposer, height: 1), - Padding(padding: const EdgeInsets.only(right: 16), child: _buildSubjectEmail(context)), - ]), - )) - ])), - const Divider(color: AppColor.colorDividerComposer, height: 1), - Padding( - padding: const EdgeInsets.only(left: 60, right: 25), - child: Column(children: [ - _buildAttachmentsWidget(context), - buildInlineLoadingView(controller), - _buildComposerEditor(context), - ]) - ) - ]) - ); - } - - Widget _buildAttachmentsWidget(BuildContext context) { - return Obx(() { - final uploadAttachments = controller.uploadController.listUploadAttachments; - if (uploadAttachments.isEmpty) { - return const SizedBox.shrink(); - } else { - return Column(children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: _buildAttachmentsTitle(context, - uploadAttachments, - controller.expandModeAttachments.value)), - Padding( - padding: const EdgeInsets.only(bottom: 8, left: 16, right: 16), - child: _buildAttachmentsList(context, - uploadAttachments, - controller.expandModeAttachments.value)) - ]); - } - }); - } - - Widget _buildComposerEditor(BuildContext context) { - return Obx(() { - final argsComposer = controller.composerArguments.value; - - if (argsComposer == null) { - return const SizedBox.shrink(); - } - - switch(argsComposer.emailActionType) { - case EmailActionType.compose: - case EmailActionType.composeFromEmailAddress: - return _buildHtmlEditor(context); - case EmailActionType.edit: - return controller.emailContentsViewState.value.fold( - (failure) => _buildHtmlEditor(context, initialContent: HtmlExtension.editorStartTags), - (success) { - if (success is GetEmailContentLoading) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: loadingWidget, - ); - } else if (success is GetEmailContentSuccess) { - var contentHtml = success.emailContents.asHtmlString; - if (contentHtml.isEmpty == true) { - contentHtml = HtmlExtension.editorStartTags; - } - return _buildHtmlEditor(context, initialContent: contentHtml); - } else { - return _buildHtmlEditor(context, initialContent: HtmlExtension.editorStartTags); - } - }); - case EmailActionType.reply: - case EmailActionType.replyAll: - case EmailActionType.forward: - var contentHtml = controller.getEmailContentQuotedAsHtml( - context, - argsComposer); - if (contentHtml.isEmpty == true) { - contentHtml = HtmlExtension.editorStartTags; + PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemWidget( + _imagePaths.icSaveToDraft, + AppLocalizations.of(context).saveAsDraft, + colorIcon: ComposerStyle.popupItemIconColor, + styleName: ComposerStyle.popupItemTextStyle, + padding: ComposerStyle.popupItemPadding, + onCallbackAction: () { + popBack(); + controller.saveToDraftAction(context); } - return _buildHtmlEditor(context, initialContent: contentHtml); - default: - return _buildHtmlEditor(context, initialContent: HtmlExtension.editorStartTags); - } - }); - } - - Widget _buildHtmlEditor(BuildContext context, {String? initialContent}) { - final richTextMobileTabletController = controller.richTextMobileTabletController; - return Padding( - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 20), - child: HtmlEditor( - key: const Key('composer_editor'), - minHeight: 550, - addDefaultSelectionMenuItems: false, - initialContent: initialContent ?? '', - onCreated: (editorApi) { - richTextMobileTabletController.htmlEditorApi = editorApi; - controller.keyboardRichTextController.onCreateHTMLEditor( - editorApi, - onEnterKeyDown: controller.onEnterKeyDown, - context: context, - ); - }, + ) ), - ); - } - - Widget _buildAttachmentsTitle( - BuildContext context, - List uploadFilesState, - ExpandMode expandModeAttachment - ) { - return Row( - children: [ - Text( - '${AppLocalizations.of(context).attachments} (${filesize(uploadFilesState.totalSize, 0)}):', - style: const TextStyle(fontSize: 12, color: AppColor.colorHintEmailAddressInput, fontWeight: FontWeight.normal)), - const Spacer(), - Material( - type: MaterialType.circle, - color: Colors.transparent, - child: TextButton( - child: Text( - expandModeAttachment == ExpandMode.EXPAND - ? AppLocalizations.of(context).hide - : '${AppLocalizations.of(context).showAll} (${uploadFilesState.length})', - style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 12, color: AppColor.colorTextButton)), - onPressed: () => controller.toggleDisplayAttachments() - ) + PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemWidget( + _imagePaths.icDeleteMailbox, + AppLocalizations.of(context).delete, + styleName: ComposerStyle.popupItemTextStyle, + padding: ComposerStyle.popupItemPadding, + onCallbackAction: () { + popBack(); + controller.closeComposer(context); + }, ) - ], - ); - } - - Widget _buildAttachmentsList( - BuildContext context, - List uploadFilesState, - ExpandMode expandMode - ) { - const double maxHeightItem = 60; - if (expandMode == ExpandMode.EXPAND) { - return LayoutBuilder(builder: (context, constraints) { - return GridView.builder( - key: const Key('list_attachment_full'), - primary: false, - shrinkWrap: true, - itemCount: uploadFilesState.length, - gridDelegate: SliverGridDelegateFixedHeight( - height: maxHeightItem, - crossAxisCount: _getMaxItemRowListAttachment(context, constraints), - crossAxisSpacing: 8.0, - mainAxisSpacing: 8.0), - itemBuilder: (context, index) => AttachmentFileComposerBuilder( - uploadFilesState[index], - onDeleteAttachmentAction: (attachment) => - controller.deleteAttachmentUploaded(attachment.uploadTaskId)) - ); - }); - } else { - return LayoutBuilder(builder: (context, constraints) { - return Align( - alignment: Alignment.centerLeft, - child: SizedBox( - height: maxHeightItem, - child: ListView.builder( - key: const Key('list_attachment_minimize'), - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - scrollDirection: Axis.horizontal, - itemCount: uploadFilesState.length, - itemBuilder: (context, index) => AttachmentFileComposerBuilder( - uploadFilesState[index], - itemMargin: const EdgeInsets.only(right: 8), - maxWidth: _getMaxWidthItemListAttachment(context, constraints), - onDeleteAttachmentAction: (attachment) => - controller.deleteAttachmentUploaded(attachment.uploadTaskId)) - ) - ) - ); - }); - } - } - - int _getMaxItemRowListAttachment(BuildContext context, BoxConstraints constraints) { - if (constraints.maxWidth < responsiveUtils.minTabletWidth) { - return 2; - } else if (constraints.maxWidth < responsiveUtils.minTabletLargeWidth) { - return 3; - } else { - return 4; - } - } - - double _getMaxWidthItemListAttachment(BuildContext context, BoxConstraints constraints) { - return constraints.maxWidth / _getMaxItemRowListAttachment(context, constraints); + ), + ]; } } \ No newline at end of file diff --git a/lib/features/composer/presentation/composer_view_web.dart b/lib/features/composer/presentation/composer_view_web.dart index d65a324e90..2fbdb58518 100644 --- a/lib/features/composer/presentation/composer_view_web.dart +++ b/lib/features/composer/presentation/composer_view_web.dart @@ -1,843 +1,736 @@ -import 'package:core/core.dart'; -import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/responsive/responsive_widget.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:filesize/filesize.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:html_editor_enhanced/html_editor.dart'; -import 'package:jmap_dart_client/jmap/identities/identity.dart'; -import 'package:model/model.dart'; +import 'package:model/email/prefix_email_address.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/base/widget/popup_item_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; -import 'package:tmail_ui_user/features/composer/presentation/mixin/composer_loading_mixin.dart'; -import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_file_composer_builder.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/email_address_input_builder.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/toolbar_rich_text_builder.dart'; -import 'package:tmail_ui_user/features/upload/presentation/extensions/list_upload_file_state_extension.dart'; -import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; -import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/composer_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/web/desktop_responsive_container_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/web/mobile_responsive_container_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/web/tablet_responsive_container_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/web/web_editor_view.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/desktop_app_bar_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/attachment_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/drop_zone_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/subject_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -class ComposerView extends GetWidget - with AppLoaderMixin, RichTextButtonMixin, ComposerLoadingMixin { +class ComposerView extends GetWidget { - final responsiveUtils = Get.find(); - final imagePaths = Get.find(); - final appToast = Get.find(); - final keyboardUtils = Get.find(); + final _responsiveUtils = Get.find(); + final _imagePaths = Get.find(); ComposerView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return ResponsiveWidget( - responsiveUtils: responsiveUtils, - mobile: Scaffold( - backgroundColor: Colors.white, - body: Container( - color: Colors.white, - child: LayoutBuilder(builder: (context, constraints) => PointerInterceptor(child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Obx(() => _buildAppBarForMobile(context, controller.isEnableEmailSendButton.value)), - const Divider(color: AppColor.colorDividerComposer, height: 1), - Obx(() => controller.identitySelected.value != null - ? _buildFromEmailAddress(context) - : const SizedBox.shrink()), - Obx(() => controller.identitySelected.value != null - ? const Divider(color: AppColor.colorDividerComposer, height: 1) - : const SizedBox.shrink()), - _buildEmailAddress(context, constraints), - const Divider(color: AppColor.colorDividerComposer, height: 1), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: _buildSubjectEmail(context)), - const Divider(color: AppColor.colorDividerComposer, height: 1), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: _buildListButton(context, constraints)), - const Divider(color: AppColor.colorDividerComposer, height: 1), - Expanded(child: Column( - children: [ - _buildAttachmentsWidget(context), - ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, - padding: const EdgeInsets.only(left: 20, top: 8, bottom: 8)), - buildInlineLoadingView(controller), - _buildEditorForm(context) - ] - )), - ] - ))) - ) - ), - desktop: Obx(() { - return Stack(children: [ - if (controller.screenDisplayMode.value == ScreenDisplayMode.normal) - Positioned(right: 5, bottom: 5, child: Card( - elevation: 20, - color: Colors.transparent, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - child: Container( - decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(24))), - width: responsiveUtils.getSizeScreenWidth(context) * 0.5, - height: responsiveUtils.getSizeScreenHeight(context) * 0.75, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(24)), - child: LayoutBuilder(builder: (context, constraints) => - PointerInterceptor(child: _buildBodyForDesktop(context, constraints))) - ) - ) - )), - if (controller.screenDisplayMode.value == ScreenDisplayMode.minimize) - Positioned(right: 5, bottom: 5, child: Card( - elevation: 20, - color: Colors.transparent, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), - child: Container( - decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(16))), - width: 500, - height: 50, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16)), - child: PointerInterceptor(child: Row(children: [ - Padding( - padding: const EdgeInsets.only(left: 10), - child: buildIconWeb( - icon: SvgPicture.asset(imagePaths.icCloseMailbox, fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).saveAndClose, - onTap: () => controller.saveEmailAsDrafts(context) - )), - buildIconWeb( - icon: SvgPicture.asset(imagePaths.icFullScreenComposer, fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).fullscreen, - onTap: () => controller.displayScreenTypeComposerAction(ScreenDisplayMode.fullScreen)), - buildIconWeb( - icon: SvgPicture.asset(imagePaths.icChevronUp, fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).show, - onTap: () => controller.displayScreenTypeComposerAction(ScreenDisplayMode.normal)), - Expanded(child: Padding( - padding: const EdgeInsets.only(left: 16, right: 80), - child: _buildTitleComposer(context), - )), - ])) - ) + responsiveUtils: _responsiveUtils, + mobile: MobileResponsiveContainerView( + childBuilder: (context, constraints) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() => MobileResponsiveAppBarComposerWidget( + isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, + isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, + openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, + isSendButtonEnabled: controller.isEnableEmailSendButton.value, + onCloseViewAction: () => controller.saveToDraftAndClose(context), + attachFileAction: () => controller.openFilePickerByType(context, FileType.any), + insertImageAction: () => controller.insertImage(context, constraints.maxWidth), + sendMessageAction: () => controller.sendEmailAction(context), + openContextMenuAction: (position) { + controller.openPopupMenuAction( + context, + position, + _createMoreOptionPopupItems(context), + radius: ComposerStyle.popupMenuRadius + ); + }, + )), + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: ComposerStyle.getMaxHeightEmailAddressWidget( + context, + constraints, + _responsiveUtils + ) + ), + child: SingleChildScrollView( + controller: controller.scrollControllerEmailAddress, + child: Obx(() => Column( + children: [ + RecipientComposerWidget( + prefix: PrefixEmailAddress.to, + listEmailAddress: controller.listToEmailAddress, + ccState: controller.ccRecipientState.value, + bccState: controller.bccRecipientState.value, + expandMode: controller.toAddressExpandMode.value, + controller: controller.toEmailAddressController, + focusNode: controller.toAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyToEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + padding: ComposerStyle.mobileRecipientPadding, + margin: ComposerStyle.mobileRecipientMargin, + nextFocusNode: controller.getNextFocusOfToEmailAddress(), + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onAddEmailAddressTypeAction: controller.addEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, + ), + if (controller.ccRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.cc, + listEmailAddress: controller.listCcEmailAddress, + expandMode: controller.ccAddressExpandMode.value, + controller: controller.ccEmailAddressController, + focusNode: controller.ccAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyCcEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.getNextFocusOfCcEmailAddress(), + padding: ComposerStyle.mobileRecipientPadding, + margin: ComposerStyle.mobileRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, + ), + if (controller.bccRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.bcc, + listEmailAddress: controller.listBccEmailAddress, + expandMode: controller.bccAddressExpandMode.value, + controller: controller.bccEmailAddressController, + focusNode: controller.bccAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyBccEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.subjectEmailInputFocusNode, + padding: ComposerStyle.mobileRecipientPadding, + margin: ComposerStyle.mobileRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, + ), + ], + )), ) + ), + SubjectComposerWidget( + focusNode: controller.subjectEmailInputFocusNode, + textController: controller.subjectEmailInputController, + onTextChange: controller.setSubjectEmail, + padding: ComposerStyle.mobileSubjectPadding, + margin: ComposerStyle.mobileSubjectMargin, + ), + Expanded( + child: LayoutBuilder(builder: (context, constraints) { + log('ComposerView::build:mobile:constraints: $constraints'); + return Stack( + children: [ + Padding( + padding: ComposerStyle.mobileEditorPadding, + child: Obx(() => WebEditorView( + editorController: controller.richTextWebController.editorController, + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + currentWebContent: controller.textEditorWeb, + onInitial: controller.handleInitHtmlEditorWeb, + onChangeContent: controller.onChangeTextEditorWeb, + onFocus: controller.handleOnFocusHtmlEditorWeb, + onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, + onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, + onEditorSettings: controller.richTextWebController.onEditorSettingsChange, + onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), + onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { + return controller.handleImageUploadFailure( + context: context, + uploadError: uploadError, + fileUpload: fileUpload, + base64Str: base64Str, + ); + }, + width: constraints.maxWidth, + height: constraints.maxHeight, + )), + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: Obx(() => InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + )), + ), + ], + ); + }), + ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return AttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + isCollapsed: controller.isAttachmentCollapsed, + onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.richTextWebController.isFormattingOptionsEnabled) { + return ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController, + padding: ComposerStyle.richToolbarPadding, + decoration: const BoxDecoration( + color: ComposerStyle.richToolbarColor, + boxShadow: ComposerStyle.richToolbarShadow + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ] + ); + } + ), + desktop: Obx(() => DesktopResponsiveContainerView( + childBuilder: (context, constraints) { + return Column(children: [ + Obx(() => DesktopAppBarComposerWidget( + emailSubject: controller.subjectEmail.value ?? '', + displayMode: controller.screenDisplayMode.value, + onCloseViewAction: () => controller.saveToDraftAndClose(context), + onChangeDisplayModeAction: controller.displayScreenTypeComposerAction, + constraints: constraints, )), - if (controller.screenDisplayMode.value == ScreenDisplayMode.fullScreen) - Scaffold( - backgroundColor: Colors.black38, - body: Align(alignment: Alignment.center, child: Card( - color: Colors.transparent, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(24))), - child: Container( - decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(24))), - width: responsiveUtils.getSizeScreenWidth(context) * 0.85, - height: responsiveUtils.getSizeScreenHeight(context) * 0.9, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(24)), - child: LayoutBuilder(builder: (context, constraints) => - PointerInterceptor(child: _buildBodyForDesktop(context, constraints))) - ) - ) - ) + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: ComposerStyle.getMaxHeightEmailAddressWidget( + context, + constraints, + _responsiveUtils ) - ) - ]); - }), - tablet: Scaffold( - backgroundColor: Colors.black38, - body: Align(alignment: Alignment.center, child: Card( - color: Colors.transparent, - shadowColor: Colors.transparent, - child: Container( - decoration: const BoxDecoration(color: Colors.white, borderRadius: BorderRadius.all(Radius.circular(24))), - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(24)), - child: LayoutBuilder(builder: (context, constraints) => - PointerInterceptor(child: _buildBodyForDesktop(context, constraints))) - ) - ) - )) - ) - ); - } - - Widget _buildAppBar(BuildContext context) { - return Row( - children: [ - buildIconWeb( - minSize: 40, - iconPadding: EdgeInsets.zero, - icon: SvgPicture.asset(imagePaths.icCloseMailbox, fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).saveAndClose, - onTap: () => controller.saveEmailAsDrafts(context)), - if (responsiveUtils.isDesktop(context)) - Obx(() => buildIconWeb( - minSize: 40, - iconPadding: EdgeInsets.zero, - icon: SvgPicture.asset( - controller.screenDisplayMode.value == ScreenDisplayMode.fullScreen - ? imagePaths.icFullScreenExit - : imagePaths.icFullScreenComposer, - fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).fullscreen, - onTap: () => controller.displayScreenTypeComposerAction(controller.screenDisplayMode.value == ScreenDisplayMode.fullScreen - ? ScreenDisplayMode.normal - : ScreenDisplayMode.fullScreen))), - if (responsiveUtils.isDesktop(context)) - buildIconWeb( - minSize: 40, - iconPadding: EdgeInsets.zero, - icon: SvgPicture.asset(imagePaths.icMinimize, fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).minimize, - onTap: () => controller.displayScreenTypeComposerAction(ScreenDisplayMode.minimize)), - Expanded(child: _buildTitleComposer(context)), - const SizedBox(width: 100), - ] - ); - } - - Widget _buildAppBarForMobile(BuildContext context, bool isEnableSendButton) { - return Container( - padding: const EdgeInsets.only(left: 10, right: 10, top: 10, bottom: 10), - color: Colors.white, - child: Row( - children: [ - buildIconWeb( - icon: SvgPicture.asset(imagePaths.icClose, width: 30, height: 30, fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).saveAndClose, - iconPadding: EdgeInsets.zero, - onTap: () => controller.saveEmailAsDrafts(context)), - Expanded(child: _buildTitleComposer(context)), - buildIconWeb( - icon: SvgPicture.asset( - isEnableSendButton ? imagePaths.icSendMobile : imagePaths.icSendDisable, - fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).send, - onTap: () => controller.sendEmailAction(context)), - buildIconWithLowerMenu( - SvgPicture.asset(imagePaths.icRequestReadReceipt), - context, - _popUpMoreActionMenu(context), - controller.openPopupMenuAction), - ] - ), - ); - } - - List _popUpMoreActionMenu(BuildContext context) { - return [ - PopupMenuItem( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: PointerInterceptor( - child: IntrinsicHeight( - child: Row( - children: [ - Obx(() => buildIconWeb( - icon: Icon(controller.hasRequestReadReceipt.value ? Icons.done : null, color: Colors.black))), - Expanded( - child: InkResponse( - child: SizedBox( - width: double.infinity, - height: double.infinity, - child: Center( - child: Text( - AppLocalizations.of(context).requestReadReceipt, - style: const TextStyle(color: Colors.black, fontSize: 15), - ) - ), + ), + child: SingleChildScrollView( + controller: controller.scrollControllerEmailAddress, + child: Obx(() => Column( + children: [ + RecipientComposerWidget( + prefix: PrefixEmailAddress.to, + listEmailAddress: controller.listToEmailAddress, + ccState: controller.ccRecipientState.value, + bccState: controller.bccRecipientState.value, + expandMode: controller.toAddressExpandMode.value, + controller: controller.toEmailAddressController, + focusNode: controller.toAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyToEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + padding: ComposerStyle.desktopRecipientPadding, + margin: ComposerStyle.desktopRecipientMargin, + nextFocusNode: controller.getNextFocusOfToEmailAddress(), + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onAddEmailAddressTypeAction: controller.addEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, ), + if (controller.ccRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.cc, + listEmailAddress: controller.listCcEmailAddress, + expandMode: controller.ccAddressExpandMode.value, + controller: controller.ccEmailAddressController, + focusNode: controller.ccAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyCcEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.getNextFocusOfCcEmailAddress(), + padding: ComposerStyle.desktopRecipientPadding, + margin: ComposerStyle.desktopRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, + ), + if (controller.bccRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.bcc, + listEmailAddress: controller.listBccEmailAddress, + expandMode: controller.bccAddressExpandMode.value, + controller: controller.bccEmailAddressController, + focusNode: controller.bccAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyBccEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.subjectEmailInputFocusNode, + padding: ComposerStyle.desktopRecipientPadding, + margin: ComposerStyle.desktopRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, + ), + ], + )), + ) + ), + SubjectComposerWidget( + focusNode: controller.subjectEmailInputFocusNode, + textController: controller.subjectEmailInputController, + onTextChange: controller.setSubjectEmail, + padding: ComposerStyle.desktopSubjectPadding, + margin: ComposerStyle.desktopSubjectMargin, + ), + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ComposerStyle.borderColor, + width: 1 + ) ), + color: ComposerStyle.backgroundEditorColor ), - ]), - ), - ), - onTap: () { - controller.toggleRequestReadReceipt(); - }, - ) - ]; - } - - Widget _buildBottomBar(BuildContext context, bool isEnableSendButton, BoxConstraints constraints) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 16), - color: Colors.white, - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: ConstrainedBox( - constraints: constraints.widthConstraints(), - child: Stack( - alignment: Alignment.centerRight, - children: [ - Row(mainAxisAlignment: MainAxisAlignment.center, - children: [ - const SizedBox(width: 24), - buildButtonWrapText( - AppLocalizations.of(context).cancel, - textStyle: const TextStyle(fontWeight: FontWeight.w500, fontSize: 17, color: AppColor.lineItemListColor), - bgColor: AppColor.emailAddressChipColor, - minWidth: 150, - height: 44, - radius: 10, - onTap: () => controller.closeComposerWeb()), - const SizedBox(width: 12), - buildButtonWrapText( - AppLocalizations.of(context).save_to_drafts, - textStyle: const TextStyle(fontWeight: FontWeight.w500, fontSize: 17, color: AppColor.colorTextButton), - bgColor: AppColor.emailAddressChipColor, - minWidth: 150, - height: 44, - radius: 10, - onTap: () => controller.saveEmailAsDrafts(context)), - const SizedBox(width: 12), - buildButtonWrapText( - AppLocalizations.of(context).send, - minWidth: 150, - height: 44, - radius: 10, - onTap: () => controller.sendEmailAction(context)), - const SizedBox(width: 24), - ] - ), - if(!responsiveUtils.isMobile(context)) - Row( - mainAxisAlignment: MainAxisAlignment.end, + child: LayoutBuilder(builder: (context, constraints) { + log('ComposerView::build:desktop:constraints: $constraints'); + return Stack( children: [ - buildIconWithUpperMenu( - SvgPicture.asset(imagePaths.icRequestReadReceipt), - context, - _popUpMoreActionMenu(context), - controller.openPopupMenuAction) - ]), - ], + Column( + children: [ + Expanded( + child: Padding( + padding: ComposerStyle.desktopEditorPadding, + child: Obx(() { + return Stack( + children: [ + WebEditorView( + editorController: controller.richTextWebController.editorController, + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + currentWebContent: controller.textEditorWeb, + onInitial: controller.handleInitHtmlEditorWeb, + onChangeContent: controller.onChangeTextEditorWeb, + onFocus: controller.handleOnFocusHtmlEditorWeb, + onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, + onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, + onEditorSettings: controller.richTextWebController.onEditorSettingsChange, + onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), + onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { + return controller.handleImageUploadFailure( + context: context, + uploadError: uploadError, + fileUpload: fileUpload, + base64Str: base64Str, + ); + }, + width: constraints.maxWidth, + height: constraints.maxHeight, + ), + if (controller.mailboxDashBoardController.isDraggableAppActive) + PointerInterceptor( + child: DropZoneWidget( + width: constraints.maxWidth, + height: constraints.maxHeight, + addAttachmentFromDropZone: controller.addAttachmentFromDropZone, + ) + ) + ], + ); + }), + ), + ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return AttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + isCollapsed: controller.isAttachmentCollapsed, + onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.richTextWebController.isFormattingOptionsEnabled) { + return ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController, + padding: ComposerStyle.richToolbarPadding, + decoration: const BoxDecoration( + color: ComposerStyle.richToolbarColor, + boxShadow: ComposerStyle.richToolbarShadow + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ], + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: Obx(() => InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + )), + ), + ], + ); + }), + ), ), - ), - ), - ); - } - - Widget _buildBodyForDesktop(BuildContext context, BoxConstraints constraints) { - return Column(children: [ - Padding(padding: const EdgeInsets.only(left: 20, right: 20, top: 8), child: _buildAppBar(context)), - const Padding(padding: EdgeInsets.only(top: 8), child: Divider(color: AppColor.colorDividerComposer, height: 1)), - Padding( - padding: const EdgeInsets.only(left: 16), - child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding(padding: const EdgeInsets.only(top: 20), - child: (AvatarBuilder() - ..text(controller.mailboxDashBoardController.userProfile.value?.getAvatarText() ?? '') - ..size(56) - ..addTextStyle(const TextStyle(fontWeight: FontWeight.w600, fontSize: 28, color: Colors.white)) - ..backgroundColor(AppColor.colorAvatar)) - .build()), - Expanded(child: Padding( - padding: const EdgeInsets.only(left: 12), - child: Column(children: [ - Obx(() => controller.identitySelected.value != null - ? _buildFromEmailAddress(context) - : const SizedBox.shrink()), - Obx(() => controller.identitySelected.value != null - ? const Divider(color: AppColor.colorDividerComposer, height: 1) - : const SizedBox.shrink()), - _buildEmailAddress(context, constraints), - const Divider(color: AppColor.colorDividerComposer, height: 1), - Padding(padding: const EdgeInsets.only(right: 16), child: _buildSubjectEmail(context)), - const Divider(color: AppColor.colorDividerComposer, height: 1), - _buildListButton(context, constraints), - ]), - )) - ])), - const Divider(color: AppColor.colorDividerComposer, height: 1), - Expanded(child: Padding( - padding: EdgeInsets.only( - left: responsiveUtils.isMobile(context) ? 16 : 60, - right: responsiveUtils.isMobile(context) ? 16 : 25), - child: Column( - children: [ - _buildAttachmentsWidget(context), - ToolbarRichTextWebBuilder(richTextWebController: controller.richTextWebController), - buildInlineLoadingView(controller), - _buildEditorForm(context) - ] - ))), - const Divider(color: AppColor.colorDividerComposer, height: 1), - Obx(() => _buildBottomBar(context, controller.isEnableEmailSendButton.value, constraints)), - ]); - } - - Widget _buildTitleComposer(BuildContext context) { - return Obx(() => Text( - controller.subjectEmail.isNotEmpty == true - ? controller.subjectEmail.value ?? '' - : AppLocalizations.of(context).new_message.capitalizeFirstEach, - maxLines: 1, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black), - )); - } - - Widget _buildFromEmailAddress(BuildContext context) { - return Padding( - padding: EdgeInsets.only( - left: responsiveUtils.isMobile(context) ? 16 : 0, - top: 12, - bottom: 12), - child: Row(children: [ - Text('${AppLocalizations.of(context).from_email_address_prefix}:', - style: const TextStyle(fontSize: 15, color: AppColor.colorHintEmailAddressInput)), - const SizedBox(width: 12), - DropdownButtonHideUnderline( - child: DropdownButton2( - isExpanded: true, - customButton: SvgPicture.asset(imagePaths.icEditIdentity), - items: controller.listIdentities.map((item) => DropdownMenuItem( - value: item, - child: PointerInterceptor( - child: Container( - alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: item == controller.identitySelected.value ? AppColor.colorBgMenuItemDropDownSelected : Colors.transparent), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - item.name ?? '', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - color: Colors.black), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap - ), - Text( - item.email ?? '', - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.normal, - color: AppColor.colorHintSearchBar), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap - ) - ] + Obx(() => BottomBarComposerWidget( + isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, + isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, + openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, + attachFileAction: () => controller.openFilePickerByType(context, FileType.any), + insertImageAction: () => controller.insertImage(context, constraints.maxWidth), + showCodeViewAction: controller.richTextWebController.toggleCodeView, + deleteComposerAction: () => controller.closeComposer(context), + saveToDraftAction: () => controller.saveToDraftAction(context), + sendMessageAction: () => controller.sendEmailAction(context), + requestReadReceiptAction: (position) { + controller.openPopupMenuAction( + context, + position, + _createReadReceiptPopupItems(context), + radius: ComposerStyle.popupMenuRadius + ); + }, + )), + ]); + }, + displayMode: controller.screenDisplayMode.value, + emailSubject: controller.subjectEmail.value ?? '', + onCloseViewAction: () => controller.saveToDraftAndClose(context), + onChangeDisplayModeAction: controller.displayScreenTypeComposerAction, + )), + tablet: TabletResponsiveContainerView( + childBuilder: (context, constraints) { + return Column(children: [ + Obx(() => DesktopAppBarComposerWidget( + emailSubject: controller.subjectEmail.value ?? '', + onCloseViewAction: () => controller.saveToDraftAndClose(context), + constraints: constraints, + )), + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: ComposerStyle.getMaxHeightEmailAddressWidget( + context, + constraints, + _responsiveUtils + ) + ), + child: SingleChildScrollView( + controller: controller.scrollControllerEmailAddress, + child: Obx(() => Column( + children: [ + RecipientComposerWidget( + prefix: PrefixEmailAddress.to, + listEmailAddress: controller.listToEmailAddress, + ccState: controller.ccRecipientState.value, + bccState: controller.bccRecipientState.value, + expandMode: controller.toAddressExpandMode.value, + controller: controller.toEmailAddressController, + focusNode: controller.toAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyToEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + padding: ComposerStyle.tabletRecipientPadding, + margin: ComposerStyle.tabletRecipientMargin, + nextFocusNode: controller.getNextFocusOfToEmailAddress(), + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onAddEmailAddressTypeAction: controller.addEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, + ), + if (controller.ccRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.cc, + listEmailAddress: controller.listCcEmailAddress, + expandMode: controller.ccAddressExpandMode.value, + controller: controller.ccEmailAddressController, + focusNode: controller.ccAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyCcEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.getNextFocusOfCcEmailAddress(), + padding: ComposerStyle.tabletRecipientPadding, + margin: ComposerStyle.tabletRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, + ), + if (controller.bccRecipientState.value == PrefixRecipientState.enabled) + RecipientComposerWidget( + prefix: PrefixEmailAddress.bcc, + listEmailAddress: controller.listBccEmailAddress, + expandMode: controller.bccAddressExpandMode.value, + controller: controller.bccEmailAddressController, + focusNode: controller.bccAddressFocusNode, + autoDisposeFocusNode: false, + keyTagEditor: controller.keyBccEmailTagEditor, + isInitial: controller.isInitialRecipient.value, + nextFocusNode: controller.subjectEmailInputFocusNode, + padding: ComposerStyle.tabletRecipientPadding, + margin: ComposerStyle.tabletRecipientMargin, + onFocusEmailAddressChangeAction: controller.onEmailAddressFocusChange, + onShowFullListEmailAddressAction: controller.showFullEmailAddress, + onDeleteEmailAddressTypeAction: controller.deleteEmailAddressType, + onUpdateListEmailAddressAction: controller.updateListEmailAddress, + onSuggestionEmailAddress: controller.getAutoCompleteSuggestion, + onFocusNextAddressAction: controller.handleFocusNextAddressAction, + onRemoveDraggableEmailAddressAction: controller.removeDraggableEmailAddress, + ), + ], + )), + ) + ), + SubjectComposerWidget( + focusNode: controller.subjectEmailInputFocusNode, + textController: controller.subjectEmailInputController, + onTextChange: controller.setSubjectEmail, + padding: ComposerStyle.tabletSubjectPadding, + margin: ComposerStyle.tabletSubjectMargin, + ), + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: ComposerStyle.borderColor, + width: 1 + ) ), + color: ComposerStyle.backgroundEditorColor ), + child: LayoutBuilder(builder: (context, constraints) { + log('ComposerView::build:tablet:constraints: $constraints'); + return Stack( + children: [ + Column( + children: [ + Expanded( + child: Padding( + padding: ComposerStyle.tabletEditorPadding, + child: Obx(() => WebEditorView( + editorController: controller.richTextWebController.editorController, + arguments: controller.composerArguments.value, + contentViewState: controller.emailContentsViewState.value, + currentWebContent: controller.textEditorWeb, + onInitial: controller.handleInitHtmlEditorWeb, + onChangeContent: controller.onChangeTextEditorWeb, + onFocus: controller.handleOnFocusHtmlEditorWeb, + onUnFocus: controller.handleOnUnFocusHtmlEditorWeb, + onMouseDown: controller.handleOnMouseDownHtmlEditorWeb, + onEditorSettings: controller.richTextWebController.onEditorSettingsChange, + onImageUploadSuccessAction: (fileUpload) => controller.handleImageUploadSuccess(context, fileUpload), + onImageUploadFailureAction: (fileUpload, base64Str, uploadError) { + return controller.handleImageUploadFailure( + context: context, + uploadError: uploadError, + fileUpload: fileUpload, + base64Str: base64Str, + ); + }, + width: constraints.maxWidth, + height: constraints.maxHeight, + )), + ), + ), + Obx(() { + if (controller.uploadController.listUploadAttachments.isNotEmpty) { + return AttachmentComposerWidget( + listFileUploaded: controller.uploadController.listUploadAttachments, + isCollapsed: controller.isAttachmentCollapsed, + onDeleteAttachmentAction: (fileState) => controller.deleteAttachmentUploaded(fileState.uploadTaskId), + onToggleExpandAttachmentAction: (isCollapsed) => controller.isAttachmentCollapsed = isCollapsed, + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.richTextWebController.isFormattingOptionsEnabled) { + return ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController, + padding: ComposerStyle.richToolbarPadding, + decoration: const BoxDecoration( + color: ComposerStyle.richToolbarColor, + boxShadow: ComposerStyle.richToolbarShadow + ), + ); + } else { + return const SizedBox.shrink(); + } + }) + ], + ), + Align( + alignment: AlignmentDirectional.topCenter, + child: Obx(() => InsertImageLoadingBarWidget( + uploadInlineViewState: controller.uploadController.uploadInlineViewState.value, + viewState: controller.viewState.value, + padding: ComposerStyle.insertImageLoadingBarPadding, + )), + ), + ], + ); + }), ), - )).toList(), - onChanged: (newIdentity) => controller.selectIdentity(newIdentity), - itemPadding: const EdgeInsets.symmetric(horizontal: 8), - customItemsHeight: 55, - dropdownMaxHeight: 240, - dropdownWidth: 370, - dropdownDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: Colors.white), - dropdownElevation: 4, - scrollbarRadius: const Radius.circular(40), - scrollbarThickness: 6, - ), - ), - Expanded(child: Padding( - padding: const EdgeInsets.only(right: 8, left: 12), - child: Text( - controller.identitySelected.value?.email ?? '', - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.normal, - color: AppColor.colorEmailAddressPrefix), - ) - )), - ]), + ), + Obx(() => BottomBarComposerWidget( + isCodeViewEnabled: controller.richTextWebController.codeViewEnabled, + isFormattingOptionsEnabled: controller.richTextWebController.isFormattingOptionsEnabled, + openRichToolbarAction: controller.richTextWebController.toggleFormattingOptions, + attachFileAction: () => controller.openFilePickerByType(context, FileType.any), + insertImageAction: () => controller.insertImage(context, constraints.maxWidth), + showCodeViewAction: controller.richTextWebController.toggleCodeView, + deleteComposerAction: () => controller.closeComposer(context), + saveToDraftAction: () => controller.saveToDraftAction(context), + sendMessageAction: () => controller.sendEmailAction(context), + requestReadReceiptAction: (position) { + controller.openPopupMenuAction( + context, + position, + _createReadReceiptPopupItems(context), + radius: ComposerStyle.popupMenuRadius + ); + }, + )), + ]); + }, + ) ); } - Widget _buildEmailAddress(BuildContext context, BoxConstraints constraints) { - log('ComposerView::_buildEmailAddress(): height: ${constraints.maxHeight}'); - return ConstrainedBox( - constraints: BoxConstraints(maxHeight: _getMaxHeightEmailAddressWidget(context, constraints)), - child: SingleChildScrollView( - child: Column( - children: [ - Obx(() => Padding( - padding: EdgeInsets.only(left: responsiveUtils.isMobile(context) ? 16 : 0), - child: (EmailAddressInputBuilder(context, imagePaths, - PrefixEmailAddress.to, - controller.listToEmailAddress, - controller.listEmailAddressType, - expandMode: controller.toAddressExpandMode.value, - controller: controller.toEmailAddressController, - focusNode: controller.toAddressFocusNode, - autoDisposeFocusNode: false, - isInitial: controller.isInitialRecipient.value, - keyTagEditor: controller.keyToEmailTagEditor - ) - ..addOnFocusEmailAddressChangeAction((prefixEmailAddress, focus) => controller.onEmailAddressFocusChange(prefixEmailAddress, focus)) - ..addOnShowFullListEmailAddressAction((prefixEmailAddress) => controller.showFullEmailAddress(prefixEmailAddress)) - ..addOnAddEmailAddressTypeAction((prefixEmailAddress) => controller.addEmailAddressType(prefixEmailAddress)) - ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) - ..addOnSuggestionEmailAddress(controller.getAutoCompleteSuggestion)) - .build() - )), - Obx(() => controller.listEmailAddressType.contains(PrefixEmailAddress.cc) == true - ? const Divider(color: AppColor.colorDividerComposer, height: 1) - : const SizedBox.shrink()), - Obx(() => controller.listEmailAddressType.contains(PrefixEmailAddress.cc) == true - ? Padding( - padding: EdgeInsets.only(left: responsiveUtils.isMobile(context) ? 16 : 0), - child: (EmailAddressInputBuilder(context, imagePaths, - PrefixEmailAddress.cc, - controller.listCcEmailAddress, - controller.listEmailAddressType, - expandMode: controller.ccAddressExpandMode.value, - controller: controller.ccEmailAddressController, - isInitial: controller.isInitialRecipient.value, - keyTagEditor: controller.keyCcEmailTagEditor - ) - ..addOnFocusEmailAddressChangeAction((prefixEmailAddress, focus) => controller.onEmailAddressFocusChange(prefixEmailAddress, focus)) - ..addOnShowFullListEmailAddressAction((prefixEmailAddress) => controller.showFullEmailAddress(prefixEmailAddress)) - ..addOnDeleteEmailAddressTypeAction((prefixEmailAddress) => controller.deleteEmailAddressType(prefixEmailAddress)) - ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) - ..addOnSuggestionEmailAddress(controller.getAutoCompleteSuggestion)) - .build()) - : const SizedBox.shrink() - ), - Obx(() => controller.listEmailAddressType.contains(PrefixEmailAddress.bcc) == true - ? const Divider(color: AppColor.colorDividerComposer, height: 1) - : const SizedBox.shrink()), - Obx(() => controller.listEmailAddressType.contains(PrefixEmailAddress.bcc) == true - ? Padding( - padding: EdgeInsets.only(left: responsiveUtils.isMobile(context) ? 16 : 0), - child: (EmailAddressInputBuilder(context, imagePaths, - PrefixEmailAddress.bcc, - controller.listBccEmailAddress, - controller.listEmailAddressType, - expandMode: controller.bccAddressExpandMode.value, - controller: controller.bccEmailAddressController, - isInitial: controller.isInitialRecipient.value, - keyTagEditor: controller.keyBccEmailTagEditor - ) - ..addOnFocusEmailAddressChangeAction((prefixEmailAddress, focus) => controller.onEmailAddressFocusChange(prefixEmailAddress, focus)) - ..addOnShowFullListEmailAddressAction((prefixEmailAddress) => controller.showFullEmailAddress(prefixEmailAddress)) - ..addOnDeleteEmailAddressTypeAction((prefixEmailAddress) => controller.deleteEmailAddressType(prefixEmailAddress)) - ..addOnUpdateListEmailAddressAction((prefixEmailAddress, listEmailAddress) => controller.updateListEmailAddress(prefixEmailAddress, listEmailAddress)) - ..addOnSuggestionEmailAddress(controller.getAutoCompleteSuggestion)) - .build()) - : const SizedBox.shrink() - ), - ], - ), + List _createReadReceiptPopupItems(BuildContext context) { + return [ + PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemWidget( + _imagePaths.icReadReceipt, + AppLocalizations.of(context).requestReadReceipt, + styleName: ComposerStyle.popupItemTextStyle, + padding: ComposerStyle.popupItemPadding, + selectedIcon: _imagePaths.icFilterSelected, + isSelected: controller.hasRequestReadReceipt.value, + onCallbackAction: () { + popBack(); + controller.toggleRequestReadReceipt(); + } ) - ); - } - - Widget _buildSubjectEmail(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(right: 8, top: 16), - child: Text( - '${AppLocalizations.of(context).subject_email}:', - style: const TextStyle(fontSize: 15, color: AppColor.colorHintEmailAddressInput))), - Expanded( - child: FocusScope(child: Focus( - onFocusChange: (focus) => controller.onSubjectEmailFocusChange(focus), - child: (TextFieldBuilder() - ..key(const Key('subject_email_input')) - ..cursorColor(AppColor.colorTextButton) - ..addFocusNode(controller.subjectEmailInputFocusNode) - ..onChange((value) => controller.setSubjectEmail(value)) - ..textStyle(const TextStyle(color: Colors.black, fontSize: 15, fontWeight: FontWeight.normal)) - ..textDecoration(const InputDecoration(contentPadding: EdgeInsets.zero, border: InputBorder.none)) - ..addController(controller.subjectEmailInputController)) - .build(), - )) - ) - ] - ); + ), + ]; } - Widget _buildListButton(BuildContext context, BoxConstraints constraints) { - return Transform( - transform: Matrix4.translationValues(-5.0, 0.0, 0.0), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 5), - child: Row(children: [ - buildIconWeb( - minSize: 40, - iconPadding: EdgeInsets.zero, - icon: SvgPicture.asset(imagePaths.icAttachmentsComposer, - width: 24, - height: 24, - color: AppColor.colorTextButton, - fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).attach_file, - onTap: () => controller.openFilePickerByType(context, FileType.any)), - const SizedBox(width: 4), - Obx(() { - final opacity = controller.richTextWebController.codeViewEnabled ? 0.5 : 1.0; - return AbsorbPointer( - absorbing: controller.richTextWebController.codeViewEnabled, - child: buildIconWeb( - minSize: 40, - iconPadding: EdgeInsets.zero, - icon: SvgPicture.asset(imagePaths.icInsertImage, - color: AppColor.colorTextButton.withOpacity(opacity), - fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).insertImage, - onTap: () => controller.insertImage(context, constraints.maxWidth)), - ); - }), - const SizedBox(width: 4), - Obx(() { - return buildIconWeb( - minSize: 40, - colorSelected: controller.richTextWebController.codeViewEnabled - ? AppColor.colorSelectedRichTextButton - : Colors.transparent, - iconPadding: EdgeInsets.zero, - icon: SvgPicture.asset(imagePaths.icStyleCodeView, - color: AppColor.colorTextButton, - fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).codeView, - onTap: () => controller.richTextWebController.toggleCodeView()); - }), - ]) + List _createMoreOptionPopupItems(BuildContext context) { + return [ + PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemWidget( + _imagePaths.icStyleCodeView, + AppLocalizations.of(context).embedCode, + styleName: ComposerStyle.popupItemTextStyle, + colorIcon: ComposerStyle.popupItemIconColor, + padding: ComposerStyle.popupItemPadding, + selectedIcon: _imagePaths.icFilterSelected, + isSelected: controller.richTextWebController.codeViewEnabled, + onCallbackAction: () { + popBack(); + controller.richTextWebController.toggleCodeView(); + } ) - ); - } - - Widget _buildEditorForm(BuildContext context) { - return Obx(() { - final argsComposer = controller.composerArguments.value; - - if (argsComposer == null) { - return const SizedBox.shrink(); - } - - final currentTextEditor = controller.textEditorWeb; - - switch(argsComposer.emailActionType) { - case EmailActionType.compose: - case EmailActionType.composeFromEmailAddress: - return _buildHtmlEditor( - context, - currentTextEditor ?? HtmlExtension.editorStartTags); - case EmailActionType.edit: - return controller.emailContentsViewState.value.fold( - (failure) => _buildHtmlEditor( - context, - currentTextEditor ?? HtmlExtension.editorStartTags), - (success) { - if (success is GetEmailContentLoading) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: loadingWidget, - ); - } else if (success is GetEmailContentSuccess) { - var contentHtml = success.emailContents.asHtmlString; - if (contentHtml.isEmpty == true) { - contentHtml = HtmlExtension.editorStartTags; - } - return _buildHtmlEditor(context, currentTextEditor ?? contentHtml); - } else { - return _buildHtmlEditor( - context, - currentTextEditor ?? HtmlExtension.editorStartTags); - } - }); - case EmailActionType.reply: - case EmailActionType.replyAll: - case EmailActionType.forward: - var contentHtml = controller.getEmailContentQuotedAsHtml( - context, - argsComposer); - if (contentHtml.isEmpty == true) { - contentHtml = HtmlExtension.editorStartTags; + ), + PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemWidget( + _imagePaths.icReadReceipt, + AppLocalizations.of(context).requestReadReceipt, + styleName: ComposerStyle.popupItemTextStyle, + padding: ComposerStyle.popupItemPadding, + colorIcon: ComposerStyle.popupItemIconColor, + selectedIcon: _imagePaths.icFilterSelected, + isSelected: controller.hasRequestReadReceipt.value, + onCallbackAction: () { + popBack(); + controller.toggleRequestReadReceipt(); } - return _buildHtmlEditor(context, currentTextEditor ?? contentHtml); - default: - return _buildHtmlEditor( - context, - currentTextEditor ?? HtmlExtension.editorStartTags); - } - }); - } - - Widget _buildHtmlEditor(BuildContext context, String initContent) { - log('ComposerView::_buildHtmlEditor(): initContent: $initContent'); - return Expanded( - child: Padding( - padding: EdgeInsets.symmetric( - horizontal: responsiveUtils.isMobile(context) ? 8 : 10), - child: HtmlEditor( - key: const Key('composer_editor_web'), - controller: controller.richTextWebController.editorController, - htmlEditorOptions: const HtmlEditorOptions( - hint: '', - darkMode: false, - customBodyCssStyle: bodyCssStyleForEditor), - blockQuotedContent: initContent, - htmlToolbarOptions: const HtmlToolbarOptions( - toolbarType: ToolbarType.hide, - defaultToolbarButtons: []), - otherOptions: const OtherOptions(height: 550), - callbacks: Callbacks(onBeforeCommand: (currentHtml) { - log('ComposerView::_buildHtmlEditor(): onBeforeCommand : $currentHtml'); - controller.setTextEditorWeb(currentHtml); - }, onChangeContent: (changed) { - log('ComposerView::_buildHtmlEditor(): onChangeContent : $changed'); - controller.setTextEditorWeb(changed); - }, onInit: () { - log('ComposerView::_buildHtmlEditor(): onInit'); - controller.setTextEditorWeb(initContent); - controller.richTextWebController.setFullScreenEditor(); - controller.richTextWebController.setEnableCodeView(); - }, onFocus: () { - log('ComposerView::_buildHtmlEditor(): onFocus'); - FocusManager.instance.primaryFocus?.unfocus(); - Future.delayed(const Duration(milliseconds: 500), () { - controller.richTextWebController.editorController.setFocus(); - }); - controller.richTextWebController.closeAllMenuPopup(); - }, onBlur: () { - controller.onEditorFocusChange(false); - }, onMouseDown: () { - Navigator.maybePop(context); - controller.onEditorFocusChange(true); - }, onChangeSelection: (settings) { - controller.richTextWebController.onEditorSettingsChange(settings); - }, onChangeCodeview: (contentChanged) { - log('ComposerView::_buildHtmlEditor(): onChangeCodeView : $contentChanged'); - controller.setTextEditorWeb(contentChanged); - }), - ) ) - ); - } - - Widget _buildAttachmentsWidget(BuildContext context) { - return Obx(() { - final attachments = controller.uploadController.listUploadAttachments; - if (attachments.isNotEmpty) { - return Column(children: [ - Padding( - padding: EdgeInsets.only( - top: 4, - bottom: 4, - left: responsiveUtils.isMobile(context) ? 16 : 20, - right: responsiveUtils.isMobile(context) ? 16: 0), - child: _buildAttachmentsTitle( - context, - attachments, - controller.expandModeAttachments.value)), - Padding( - padding: EdgeInsets.only( - bottom: 8, - left: responsiveUtils.isMobile(context) ? 16 : 10, - right: responsiveUtils.isMobile(context) ? 16 : 10), - child: _buildAttachmentsList( - context, - attachments, - controller.expandModeAttachments.value)) - ]); - } else { - return const SizedBox.shrink(); - } - }); - } - - Widget _buildAttachmentsTitle( - BuildContext context, - List uploadFilesState, - ExpandMode expandModeAttachment) { - return Row( - children: [ - Text( - '${AppLocalizations.of(context).attachments} (${filesize(uploadFilesState.totalSize, 0)}):', - style: const TextStyle(fontSize: 12, color: AppColor.colorHintEmailAddressInput, fontWeight: FontWeight.normal)), - const Spacer(), - Material( - type: MaterialType.circle, - color: Colors.transparent, - child: TextButton( - child: Text( - expandModeAttachment == ExpandMode.EXPAND - ? AppLocalizations.of(context).hide - : '${AppLocalizations.of(context).showAll} (${uploadFilesState.length})', - style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 12, color: AppColor.colorTextButton)), - onPressed: () => controller.toggleDisplayAttachments() - ) + ), + PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemWidget( + _imagePaths.icSaveToDraft, + AppLocalizations.of(context).saveAsDraft, + colorIcon: ComposerStyle.popupItemIconColor, + styleName: ComposerStyle.popupItemTextStyle, + padding: ComposerStyle.popupItemPadding, + onCallbackAction: () { + popBack(); + controller.saveToDraftAction(context); + } ) - ], - ); - } - - Widget _buildAttachmentsList( - BuildContext context, - List uploadFilesState, - ExpandMode expandMode) { - if (expandMode == ExpandMode.COLLAPSE) { - return const SizedBox.shrink(); - } else { - return LayoutBuilder(builder: (context, constraints) { - return Align( - alignment: Alignment.centerLeft, - child: SizedBox( - height: 60, - child: ListView.builder( - key: const Key('list_attachment_minimize'), - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - scrollDirection: Axis.horizontal, - itemCount: uploadFilesState.length, - itemBuilder: (context, index) => AttachmentFileComposerBuilder( - uploadFilesState[index], - itemMargin: const EdgeInsets.only(right: 8), - maxWidth: _getMaxWidthItemListAttachment(context, constraints), - onDeleteAttachmentAction: (attachment) => - controller.deleteAttachmentUploaded(attachment.uploadTaskId)) - ) - ) - ); - }); - } - } - - int _getMaxItemRowListAttachment(BuildContext context, BoxConstraints constraints) { - if (constraints.maxWidth < responsiveUtils.minTabletWidth) { - return 2; - } else if (constraints.maxWidth < responsiveUtils.minTabletLargeWidth) { - return 3; - } else { - return 4; - } - } - - double _getMaxWidthItemListAttachment(BuildContext context, BoxConstraints constraints) { - final currentWidth = constraints.maxWidth - 40; - return currentWidth / _getMaxItemRowListAttachment(context, constraints); - } - - double _getMaxHeightEmailAddressWidget(BuildContext context, BoxConstraints constraints) { - if (responsiveUtils.isDesktop(context)) { - return constraints.maxHeight > 0 ? constraints.maxHeight * 0.3 : 150.0; - } else { - return constraints.maxHeight > 0 ? constraints.maxHeight * 0.4 : 150.0; - } + ), + PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemWidget( + _imagePaths.icDeleteMailbox, + AppLocalizations.of(context).delete, + styleName: ComposerStyle.popupItemTextStyle, + padding: ComposerStyle.popupItemPadding, + onCallbackAction: () { + popBack(); + controller.closeComposer(context); + }, + ) + ), + ]; } } \ No newline at end of file diff --git a/lib/features/composer/presentation/controller/base_rich_text_controller.dart b/lib/features/composer/presentation/controller/base_rich_text_controller.dart index 86aee1fff5..d2e2b1b199 100644 --- a/lib/features/composer/presentation/controller/base_rich_text_controller.dart +++ b/lib/features/composer/presentation/controller/base_rich_text_controller.dart @@ -22,7 +22,7 @@ abstract class BaseRichTextController extends GetxController { ) async { await ColorPickerDialogBuilder( context, - currentColor, + ValueNotifier(currentColor), title: AppLocalizations.of(context).chooseAColor, textActionSetColor: AppLocalizations.of(context).setColor, textActionResetDefault: AppLocalizations.of(context).resetToDefault, diff --git a/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart b/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart index 7e321968af..43a0fa9121 100644 --- a/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart +++ b/lib/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart @@ -1,5 +1,6 @@ -import 'package:core/core.dart'; -import 'package:enough_html_editor/enough_html_editor.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/base_rich_text_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; @@ -8,12 +9,20 @@ import 'package:tmail_ui_user/features/composer/presentation/model/inline_image. class RichTextMobileTabletController extends BaseRichTextController { HtmlEditorApi? htmlEditorApi; - void insertImage(InlineImage image, {double? maxWithEditor}) async { - log('RichTextMobileTabletController::insertImage(): $image | maxWithEditor: $maxWithEditor'); + void insertImage( + InlineImage image, + { + double? maxWithEditor, + bool fromFileShare = false + } + ) async { + log('RichTextMobileTabletController::insertImage(): $image | maxWithEditor: $maxWithEditor | $fromFileShare'); if (image.source == ImageSource.network) { htmlEditorApi?.insertImageLink(image.link!); } else { - await htmlEditorApi?.moveCursorAtLastNode(); + if (fromFileShare) { + await htmlEditorApi?.moveCursorAtLastNode(); + } await htmlEditorApi?.insertHtml(image.base64Uri ?? ''); } } @@ -22,4 +31,16 @@ class RichTextMobileTabletController extends BaseRichTextController { final styleSelected = newStyle ?? HeaderStyleType.normal; htmlEditorApi?.formatHeader(styleSelected.styleValue); } + + void insertImageAsBase64({required PlatformFile platformFile, int? maxWidth}) async { + if (platformFile.bytes != null) { + await htmlEditorApi?.insertImageData( + platformFile.bytes!, + 'image/${platformFile.extension}', + maxWidth: maxWidth + ); + } else { + logError("RichTextWebController::insertImageAsBase64: bytes is null"); + } + } } diff --git a/lib/features/composer/presentation/controller/rich_text_web_controller.dart b/lib/features/composer/presentation/controller/rich_text_web_controller.dart index 97ffd52cf1..c639c0b9c0 100644 --- a/lib/features/composer/presentation/controller/rich_text_web_controller.dart +++ b/lib/features/composer/presentation/controller/rich_text_web_controller.dart @@ -1,12 +1,16 @@ +import 'dart:convert'; + import 'package:core/presentation/extensions/color_extension.dart'; import 'package:custom_pop_up_menu/custom_pop_up_menu.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:html_editor_enhanced/html_editor.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/code_view_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/dropdown_menu_font_status.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/formatting_options_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; @@ -19,7 +23,7 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class RichTextWebController extends BaseRichTextController { - final editorController = HtmlEditorController(processNewLineAsBr: true); + final editorController = HtmlEditorController(); final listTextStyleApply = RxList(); final selectedTextColor = Colors.black.obs; @@ -32,6 +36,7 @@ class RichTextWebController extends BaseRichTextController { final focusMenuParagraph = RxBool(false); final menuFontStatus = DropdownMenuFontStatus.closed.obs; final menuHeaderStyleStatus = DropdownMenuFontStatus.closed.obs; + final formattingOptionsState = FormattingOptionsState.disabled.obs; final menuParagraphController = CustomPopupMenuController(); final menuOrderListController = CustomPopupMenuController(); @@ -47,8 +52,16 @@ class RichTextWebController extends BaseRichTextController { }); } - void onEditorSettingsChange(EditorSettings settings) async { - log('RichTextWebController::onEditorSettingsChange():'); + void onEditorSettingsChange(EditorSettings settings) { + _updateTextStyle(settings); + _updateFontName(settings); + _updateTextColor(settings); + _updateBackgroundTextColor(settings); + _updateOrderList(settings); + _updateParagraph(settings); + } + + void _updateTextStyle(EditorSettings settings) { listTextStyleApply.clear(); if (settings.isBold) { @@ -67,68 +80,91 @@ class RichTextWebController extends BaseRichTextController { listTextStyleApply.add(RichTextStyleType.strikeThrough); } - log('RichTextWebController::onEditorSettingsChange(): $listTextStyleApply'); + log('RichTextWebController::_updateTextStyle(): $listTextStyleApply'); + } + + void _updateFontName(EditorSettings settings) { + log('RichTextWebController::_updateFontName():fontName: ${settings.fontName}'); + final matchedFontName = FontNameType.values.firstWhereOrNull((fontName) => fontName.value == settings.fontName); + log('RichTextWebController::_updateFontName():matchedFontName: $matchedFontName'); + if (matchedFontName != null) { + selectedFontName.value = matchedFontName; + } + } + + void _updateTextColor(EditorSettings settings) { + selectedTextColor.value = settings.foregroundColor; + } + + void _updateBackgroundTextColor(EditorSettings settings) { + selectedTextBackgroundColor.value = settings.backgroundColor; + } + + void _updateOrderList(EditorSettings settings) { + if (settings.isOl) { + selectedOrderList.value = OrderListType.numberedList; + } else if (settings.isUl) { + selectedOrderList.value = OrderListType.bulletedList; + } + } + + void _updateParagraph(EditorSettings settings) { + if (settings.isAlignCenter) { + selectedParagraph.value = ParagraphType.alignCenter; + } else if (settings.isAlignJustify) { + selectedParagraph.value = ParagraphType.justify; + } else if (settings.isAlignLeft) { + selectedParagraph.value = ParagraphType.alignLeft; + } else if (settings.isAlignRight) { + selectedParagraph.value = ParagraphType.alignRight; + } } void applyRichTextStyle(BuildContext context, RichTextStyleType textStyleType) { switch(textStyleType) { case RichTextStyleType.textColor: openMenuSelectColor( - context, - selectedTextColor.value, - onResetToDefault: () { - final colorAsString = Colors.black.toHexTriplet(); - selectedTextColor.value = Colors.black; - editorController.execCommand( - textStyleType.commandAction, - argument: colorAsString); - editorController.setFocus(); - }, - onSelectColor: (selectedColor) { - final newColor = selectedColor ?? Colors.black; - final colorAsString = newColor.toHexTriplet(); - log('RichTextWebController::applyRichTextStyle():selectedTextColor: colorAsString: $colorAsString'); - selectedTextColor.value = newColor; - editorController.execCommand( - textStyleType.commandAction, - argument: colorAsString); - editorController.setFocus(); - } + context, + selectedTextColor.value, + onResetToDefault: () => _applyForegroundColor(Colors.black), + onSelectColor: _applyForegroundColor ); break; case RichTextStyleType.textBackgroundColor: openMenuSelectColor( - context, - selectedTextBackgroundColor.value, - onResetToDefault: () { - final colorAsString = Colors.white.toHexTriplet(); - log('RichTextWebController::applyRichTextStyle():onResetToDefault: colorAsString: $colorAsString'); - selectedTextBackgroundColor.value = Colors.white; - editorController.execCommand( - textStyleType.commandAction, - argument: colorAsString); - editorController.setFocus(); - }, - onSelectColor: (selectedColor) { - final newColor = selectedColor ?? Colors.white; - final colorAsString = newColor.toHexTriplet(); - log('RichTextWebController::applyRichTextStyle():textBackgroundColor: colorAsString: $colorAsString'); - selectedTextBackgroundColor.value = newColor; - editorController.execCommand( - textStyleType.commandAction, - argument: colorAsString); - editorController.setFocus(); - } + context, + selectedTextBackgroundColor.value, + onResetToDefault: () => applyBackgroundColor(Colors.white), + onSelectColor: applyBackgroundColor ); break; default: - editorController.execCommand(textStyleType.commandAction); + editorController.execSummernoteAPI(textStyleType.summernoteNameAPI); _selectTextStyleType(textStyleType); - editorController.setFocus(); break; } } + void _applyForegroundColor(Color? selectedColor) { + final newColor = selectedColor ?? Colors.black; + final colorAsString = newColor.toHexTriplet(); + log('RichTextWebController::_applyForegroundColor():colorAsString: $colorAsString'); + selectedTextColor.value = newColor; + editorController.execSummernoteAPI( + RichTextStyleType.textColor.summernoteNameAPI, + value: colorAsString); + } + + void applyBackgroundColor(Color? selectedColor) { + final newColor = selectedColor ?? Colors.white; + final colorAsString = newColor.toHexTriplet(); + log('RichTextWebController::_applyBackgroundColor():colorAsString: $colorAsString'); + selectedTextBackgroundColor.value = newColor; + editorController.execSummernoteAPI( + RichTextStyleType.textBackgroundColor.summernoteNameAPI, + value: colorAsString); + } + void _selectTextStyleType(RichTextStyleType textStyleType) { if (listTextStyleApply.contains(textStyleType)) { listTextStyleApply.remove(textStyleType); @@ -145,17 +181,22 @@ class RichTextWebController extends BaseRichTextController { if (image.source == ImageSource.network) { editorController.insertNetworkImage(image.link!); } else { - editorController.insertHtml(image.base64Uri ?? ''); + editorController.insertHtml("
${image.base64Uri ?? ''}

"); } } + void insertInlineImage({required String base64Data, String? name}) async { + log('RichTextWebController::insertInlineImage():name: $name'); + final base64Uri = 'Inline Image TeamMail'; + editorController.insertHtml(base64Uri); + } + void applyNewFontStyle(FontNameType? newFont) { final fontSelected = newFont ?? FontNameType.sansSerif; selectedFontName.value = fontSelected; - editorController.execCommand( - RichTextStyleType.fontName.commandAction, - argument: fontSelected.fontFamily); - editorController.setFocus(); + editorController.execSummernoteAPI( + RichTextStyleType.fontName.summernoteNameAPI, + value: fontSelected.value); } bool get isMenuFontOpen => menuFontStatus.value == DropdownMenuFontStatus.open; @@ -174,10 +215,6 @@ class RichTextWebController extends BaseRichTextController { Future get isActivatedCodeView => editorController.isActivatedCodeView(); - void setFullScreenEditor() { - editorController.setFullScreen(); - } - void setEnableCodeView() async { final isActivated = await isActivatedCodeView; if (codeViewEnabled && !isActivated) { @@ -190,25 +227,24 @@ class RichTextWebController extends BaseRichTextController { final newCodeViewState = isActivated ? CodeViewState.disabled : CodeViewState.enabled; codeViewState.value = newCodeViewState; editorController.toggleCodeView(); - if (isActivated) { - setFullScreenEditor(); - editorController.setFullScreen(); - } } void applyHeaderStyle(HeaderStyleType? newStyle) { final styleSelected = newStyle ?? HeaderStyleType.normal; - editorController.execCommand( + if (styleSelected == HeaderStyleType.blockquote || styleSelected == HeaderStyleType.code) { + editorController.execCommand( RichTextStyleType.headerStyle.commandAction, argument: styleSelected.styleValue); - editorController.setFocus(); + editorController.setFocus(); + } else { + editorController.execSummernoteAPI(styleSelected.summernoteNameAPI); + } } void applyParagraphType(ParagraphType newParagraph) { selectedParagraph.value = newParagraph; - editorController.execCommand(newParagraph.commandAction); + editorController.execSummernoteAPI(newParagraph.summernoteNameAPI); menuParagraphController.hideMenu(); - editorController.setFocus(); } void closeAllMenuPopup() { @@ -228,11 +264,31 @@ class RichTextWebController extends BaseRichTextController { void applyOrderListType(OrderListType newOrderList) { selectedOrderList.value = newOrderList; - editorController.execCommand(newOrderList.commandAction); + editorController.execSummernoteAPI(newOrderList.summernoteNameAPI); menuOrderListController.hideMenu(); - editorController.setFocus(); } + void insertImageAsBase64({required PlatformFile platformFile}) { + if (platformFile.bytes != null) { + final base64Data = base64Encode(platformFile.bytes!); + editorController.insertHtml( + 'Image in my signature' + ); + } else { + logError("RichTextWebController::insertImageAsBase64: bytes is null"); + } + } + + void toggleFormattingOptions() { + final newState = isFormattingOptionsEnabled + ? FormattingOptionsState.disabled + : FormattingOptionsState.enabled; + + formattingOptionsState.value = newState; + } + + bool get isFormattingOptionsEnabled => formattingOptionsState.value == FormattingOptionsState.enabled; + @override void onClose() { menuParagraphController.dispose(); diff --git a/lib/features/composer/presentation/extensions/email_action_type_extension.dart b/lib/features/composer/presentation/extensions/email_action_type_extension.dart index 362507bb37..8a7f35dc8e 100644 --- a/lib/features/composer/presentation/extensions/email_action_type_extension.dart +++ b/lib/features/composer/presentation/extensions/email_action_type_extension.dart @@ -1,25 +1,35 @@ +import 'package:core/presentation/extensions/html_extension.dart'; import 'package:flutter/cupertino.dart'; import 'package:model/email/email_action_type.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/list_email_address_extension.dart'; +import 'package:model/extensions/utc_date_extension.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; extension EmailActionTypeExtension on EmailActionType { - String getSubjectComposer(BuildContext context, String subject) { + String getSubjectComposer(BuildContext? context, String subject) { switch(this) { case EmailActionType.reply: case EmailActionType.replyAll: if (subject.toLowerCase().startsWith('re:')) { return subject; } else { - return '${AppLocalizations.of(context).prefix_reply_email} $subject'; + return context != null + ? '${AppLocalizations.of(context).prefix_reply_email} $subject' + : subject; } case EmailActionType.forward: if (subject.toLowerCase().startsWith('fwd:')) { return subject; } else { - return '${AppLocalizations.of(context).prefix_forward_email} $subject'; + return context != null + ? '${AppLocalizations.of(context).prefix_forward_email} $subject' + : subject; } - case EmailActionType.edit: + case EmailActionType.editDraft: + case EmailActionType.editSendingEmail: + case EmailActionType.reopenComposerBrowser: return subject; default: return ''; @@ -40,4 +50,71 @@ extension EmailActionTypeExtension on EmailActionType { return ''; } } + + String? getHeaderEmailQuoted({ + required BuildContext context, + required PresentationEmail presentationEmail + }) { + final locale = Localizations.localeOf(context).toLanguageTag(); + switch(this) { + case EmailActionType.reply: + case EmailActionType.replyAll: + final receivedAt = presentationEmail.receivedAt; + final emailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true); + return AppLocalizations.of(context).header_email_quoted( + receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale), + emailAddress + ); + case EmailActionType.forward: + var headerQuoted = '------- ${AppLocalizations.of(context).forwarded_message} -------'.addNewLineTag(); + + final subject = presentationEmail.subject ?? ''; + final receivedAt = presentationEmail.receivedAt; + final fromEmailAddress = presentationEmail.from.listEmailAddressToString(isFullEmailAddress: true); + final toEmailAddress = presentationEmail.to.listEmailAddressToString(isFullEmailAddress: true); + final ccEmailAddress = presentationEmail.cc.listEmailAddressToString(isFullEmailAddress: true); + final bccEmailAddress = presentationEmail.bcc.listEmailAddressToString(isFullEmailAddress: true); + + if (subject.isNotEmpty) { + headerQuoted = headerQuoted + .append('${AppLocalizations.of(context).subject_email}: ') + .append(subject) + .addNewLineTag(); + } + if (receivedAt != null) { + headerQuoted = headerQuoted + .append('${AppLocalizations.of(context).date}: ') + .append(receivedAt.formatDateToLocal(pattern: 'MMM d, y h:mm a', locale: locale)) + .addNewLineTag(); + } + if (fromEmailAddress.isNotEmpty) { + headerQuoted = headerQuoted + .append('${AppLocalizations.of(context).from_email_address_prefix}: ') + .append(fromEmailAddress) + .addNewLineTag(); + } + if (toEmailAddress.isNotEmpty) { + headerQuoted = headerQuoted + .append('${AppLocalizations.of(context).to_email_address_prefix}: ') + .append(toEmailAddress) + .addNewLineTag(); + } + if (ccEmailAddress.isNotEmpty) { + headerQuoted = headerQuoted + .append('${AppLocalizations.of(context).cc_email_address_prefix}: ') + .append(ccEmailAddress) + .addNewLineTag(); + } + if (bccEmailAddress.isNotEmpty) { + headerQuoted = headerQuoted + .append('${AppLocalizations.of(context).bcc_email_address_prefix}: ') + .append(bccEmailAddress) + .addNewLineTag(); + } + + return headerQuoted; + default: + return null; + } + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/file_upload_extension.dart b/lib/features/composer/presentation/extensions/file_upload_extension.dart new file mode 100644 index 0000000000..55a9a35662 --- /dev/null +++ b/lib/features/composer/presentation/extensions/file_upload_extension.dart @@ -0,0 +1,45 @@ +import 'dart:convert' as convert; +import 'dart:typed_data' as type_data; + +import 'package:flutter/foundation.dart'; +import 'package:get/get_connect/http/src/request/request.dart'; +import 'package:html_editor_enhanced/utils/file_upload_model.dart'; +import 'package:model/upload/file_info.dart'; + +extension FileUploadExtension on FileUpload { + + String? get base64Data { + if (base64 != null) { + if (!base64!.contains(',')) { + return base64; + } + final listData = base64!.split(','); + if (listData.length < 2) { + return base64; + } + + final base64Origin = listData[1]; + return base64Origin; + } + return base64; + } + + Future toFileInfo() async { + if (base64Data != null) { + final bytesStream = await compute(convertBytesToStream, base64Data!); + return FileInfo.fromStream( + stream: bytesStream, + name: name, + size: size + ); + } else { + return null; + } + } + + static Stream> convertBytesToStream(String base64) { + type_data.Uint8List decodeBytes = convert.base64Decode(base64); + final bytesStream = BodyBytesStream.fromBytes(decodeBytes); + return bytesStream; + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/extensions/list_identities_extension.dart b/lib/features/composer/presentation/extensions/list_identities_extension.dart new file mode 100644 index 0000000000..5d3baa8672 --- /dev/null +++ b/lib/features/composer/presentation/extensions/list_identities_extension.dart @@ -0,0 +1,7 @@ + +import 'package:jmap_dart_client/jmap/identities/identity.dart'; + +extension ListIdentitiesExtension on List { + + List toListMayDeleted() => where((identity) => identity.mayDelete == true).toList(); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/mixin/composer_loading_mixin.dart b/lib/features/composer/presentation/mixin/composer_loading_mixin.dart deleted file mode 100644 index 01091239f1..0000000000 --- a/lib/features/composer/presentation/mixin/composer_loading_mixin.dart +++ /dev/null @@ -1,45 +0,0 @@ - -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart'; -import 'package:tmail_ui_user/features/composer/presentation/composer_controller.dart'; -import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; - -mixin ComposerLoadingMixin { - - Widget _loadingWidgetWithSizeColor({double? size, Color? color}) { - return Center(child: Container( - margin: const EdgeInsets.all(10), - width: size ?? 24, - height: size ?? 24, - child: CircularProgressIndicator(color: color ?? AppColor.primaryColor))); - } - - Widget buildInlineLoadingView(ComposerController controller) { - return Obx(() => controller.uploadController.uploadInlineViewState.value.fold( - (failure) { - return controller.viewState.value.fold( - (failure) => const SizedBox.shrink(), - (success) { - if (success is DownloadingImageAsBase64) { - return _loadingWidgetWithSizeColor(); - } - return const SizedBox.shrink(); - }); - }, - (success) { - if (success is UploadingAttachmentUploadState) { - return _loadingWidgetWithSizeColor(); - } - return controller.viewState.value.fold( - (failure) => const SizedBox.shrink(), - (success) { - if (success is DownloadingImageAsBase64) { - return _loadingWidgetWithSizeColor(); - } - return const SizedBox.shrink(); - }); - })); - } -} \ No newline at end of file diff --git a/lib/features/composer/presentation/mixin/rich_text_button_mixin.dart b/lib/features/composer/presentation/mixin/rich_text_button_mixin.dart index 97531131c6..484c277cf2 100644 --- a/lib/features/composer/presentation/mixin/rich_text_button_mixin.dart +++ b/lib/features/composer/presentation/mixin/rich_text_button_mixin.dart @@ -96,9 +96,9 @@ mixin RichTextButtonMixin { return buildIconWeb( icon: SvgPicture.asset( path, - color: isSelected == true - ? Colors.black.withOpacity(opacity) - : AppColor.colorDefaultRichTextButton.withOpacity(opacity), + colorFilter: isSelected == true + ? Colors.black.withOpacity(opacity).asFilter() + : AppColor.colorDefaultRichTextButton.withOpacity(opacity).asFilter(), fit: BoxFit.fill), iconPadding: const EdgeInsets.all(4), colorFocus: Colors.white, @@ -120,12 +120,14 @@ mixin RichTextButtonMixin { return tooltip?.isNotEmpty == true ? Tooltip( - child: SvgPicture.asset(path, - color: newColor?.withOpacity(opacity), - fit: BoxFit.fill), - message: tooltip) - : SvgPicture.asset(path, - color: newColor?.withOpacity(opacity), + message: tooltip, + child: SvgPicture.asset( + path, + colorFilter: newColor?.withOpacity(opacity).asFilter(), + fit: BoxFit.fill)) + : SvgPicture.asset( + path, + colorFilter: newColor?.withOpacity(opacity).asFilter(), fit: BoxFit.fill); } @@ -138,9 +140,10 @@ mixin RichTextButtonMixin { ? AppColor.colorDefaultRichTextButton : color; - return SvgPicture.asset(path, - color: newColor?.withOpacity(opacity), - fit: BoxFit.fill); + return SvgPicture.asset( + path, + colorFilter: newColor?.withOpacity(opacity).asFilter(), + fit: BoxFit.fill); } Widget buildIconColorBackgroundTextWithoutTooltip({ @@ -166,10 +169,10 @@ mixin RichTextButtonMixin { ? AppColor.colorDefaultRichTextButton : colorSelected; return Tooltip( + message: tooltip, child: Icon(iconData, color: (newColor ?? AppColor.colorDefaultRichTextButton).withOpacity(opacity), size: 20), - message: tooltip, ); } @@ -223,9 +226,10 @@ mixin RichTextButtonMixin { DropDownMenuHeaderStyleWidget( icon: buildWrapIconStyleText( isSelected: richTextController.isMenuHeaderStyleOpen, - icon: SvgPicture.asset(RichTextStyleType.headerStyle.getIcon(_imagePaths), - color: AppColor.colorDefaultRichTextButton, - fit: BoxFit.fill), + icon: SvgPicture.asset( + RichTextStyleType.headerStyle.getIcon(_imagePaths), + colorFilter: AppColor.colorDefaultRichTextButton.asFilter(), + fit: BoxFit.fill), padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), tooltip: RichTextStyleType.headerStyle.getTooltipButton(context) ), diff --git a/lib/features/composer/presentation/model/compose_action_mode.dart b/lib/features/composer/presentation/model/compose_action_mode.dart new file mode 100644 index 0000000000..b6c10166bc --- /dev/null +++ b/lib/features/composer/presentation/model/compose_action_mode.dart @@ -0,0 +1,6 @@ + +enum ComposeActionMode { + pushQueue, + editQueue, + sent +} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/draggable_email_address.dart b/lib/features/composer/presentation/model/draggable_email_address.dart new file mode 100644 index 0000000000..e5347b3658 --- /dev/null +++ b/lib/features/composer/presentation/model/draggable_email_address.dart @@ -0,0 +1,16 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/email/prefix_email_address.dart'; + +class DraggableEmailAddress with EquatableMixin { + final EmailAddress emailAddress; + final PrefixEmailAddress prefix; + + DraggableEmailAddress({ + required this.emailAddress, + required this.prefix, + }); + + @override + List get props => [emailAddress, prefix]; +} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/font_name_type.dart b/lib/features/composer/presentation/model/font_name_type.dart index 25788be6d5..37922a8bf6 100644 --- a/lib/features/composer/presentation/model/font_name_type.dart +++ b/lib/features/composer/presentation/model/font_name_type.dart @@ -15,7 +15,7 @@ enum FontNameType { sansSerif, verdana; - String get fontFamily { + String get title { switch(this) { case FontNameType.arial: return 'Arial'; @@ -47,4 +47,37 @@ enum FontNameType { return 'Verdana'; } } + + String get value { + switch(this) { + case FontNameType.arial: + return 'Arial'; + case FontNameType.arialBlack: + return 'Arial Black'; + case FontNameType.brushScriptMT: + return 'Brush Script MT'; + case FontNameType.comicSansMS: + return 'Comic Sans MS'; + case FontNameType.courierNew: + return 'Courier New'; + case FontNameType.helveticaNeue: + return 'Helvetica Neue'; + case FontNameType.helvetica: + return 'Helvetica'; + case FontNameType.impact: + return 'Impact'; + case FontNameType.lucidaGrande: + return 'Lucida Grande'; + case FontNameType.tahoma: + return 'Tahoma'; + case FontNameType.timesNewRoman: + return 'Times New Roman'; + case FontNameType.trebuchetMS: + return 'Trebuchet MS'; + case FontNameType.sansSerif: + return 'sans-serif'; + case FontNameType.verdana: + return 'Verdana'; + } + } } \ No newline at end of file diff --git a/lib/features/composer/presentation/model/formatting_options_state.dart b/lib/features/composer/presentation/model/formatting_options_state.dart new file mode 100644 index 0000000000..985d5da7ce --- /dev/null +++ b/lib/features/composer/presentation/model/formatting_options_state.dart @@ -0,0 +1,5 @@ + +enum FormattingOptionsState { + enabled, + disabled +} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/header_style_type.dart b/lib/features/composer/presentation/model/header_style_type.dart index cbe3c04d81..ad82c5667a 100644 --- a/lib/features/composer/presentation/model/header_style_type.dart +++ b/lib/features/composer/presentation/model/header_style_type.dart @@ -58,6 +58,27 @@ enum HeaderStyleType { } } + String get summernoteNameAPI { + switch (this) { + case HeaderStyleType.normal: + return 'formatPara'; + case HeaderStyleType.h1: + return 'formatH1'; + case HeaderStyleType.h2: + return 'formatH2'; + case HeaderStyleType.h3: + return 'formatH3'; + case HeaderStyleType.h4: + return 'formatH4'; + case HeaderStyleType.h5: + return 'formatH5'; + case HeaderStyleType.h6: + return 'formatH6'; + default: + return ''; + } + } + double get textSize { switch(this) { case HeaderStyleType.normal: diff --git a/lib/features/composer/presentation/model/order_list_type.dart b/lib/features/composer/presentation/model/order_list_type.dart index b960aabb1e..57e4b4fb75 100644 --- a/lib/features/composer/presentation/model/order_list_type.dart +++ b/lib/features/composer/presentation/model/order_list_type.dart @@ -17,6 +17,15 @@ enum OrderListType { } } + String get summernoteNameAPI { + switch(this) { + case OrderListType.bulletedList: + return 'insertUnorderedList'; + case OrderListType.numberedList: + return 'insertOrderedList'; + } + } + String getIcon(ImagePaths imagePaths) { switch (this) { case OrderListType.bulletedList: diff --git a/lib/features/composer/presentation/model/paragraph_type.dart b/lib/features/composer/presentation/model/paragraph_type.dart index 927fe8138f..c85238fbf0 100644 --- a/lib/features/composer/presentation/model/paragraph_type.dart +++ b/lib/features/composer/presentation/model/paragraph_type.dart @@ -29,6 +29,23 @@ enum ParagraphType { } } + String get summernoteNameAPI { + switch(this) { + case ParagraphType.alignLeft: + return 'justifyLeft'; + case ParagraphType.alignRight: + return 'justifyRight'; + case ParagraphType.alignCenter: + return 'justifyCenter'; + case ParagraphType.justify: + return 'justifyFull'; + case ParagraphType.indent: + return 'indent'; + case ParagraphType.outdent: + return 'outdent'; + } + } + String getIcon(ImagePaths imagePaths) { switch (this) { case ParagraphType.alignLeft: diff --git a/lib/features/composer/presentation/model/prefix_recipient_state.dart b/lib/features/composer/presentation/model/prefix_recipient_state.dart new file mode 100644 index 0000000000..4c11ebac77 --- /dev/null +++ b/lib/features/composer/presentation/model/prefix_recipient_state.dart @@ -0,0 +1,5 @@ + +enum PrefixRecipientState { + enabled, + disabled +} \ No newline at end of file diff --git a/lib/features/composer/presentation/model/rich_text_style_type.dart b/lib/features/composer/presentation/model/rich_text_style_type.dart index baefb60c7b..0eaac828f5 100644 --- a/lib/features/composer/presentation/model/rich_text_style_type.dart +++ b/lib/features/composer/presentation/model/rich_text_style_type.dart @@ -38,6 +38,27 @@ enum RichTextStyleType { } } + String get summernoteNameAPI { + switch (this) { + case RichTextStyleType.textColor: + return 'foreColor'; + case RichTextStyleType.textBackgroundColor: + return 'backColor'; + case RichTextStyleType.fontName: + return 'fontName'; + case bold: + return 'bold'; + case italic: + return 'italic'; + case underline: + return 'underline'; + case strikeThrough: + return 'strikethrough'; + default: + return ''; + } + } + String getIcon(ImagePaths imagePaths) { switch (this) { case bold: diff --git a/lib/features/composer/presentation/model/save_to_draft_view_event.dart b/lib/features/composer/presentation/model/save_to_draft_view_event.dart new file mode 100644 index 0000000000..14db275c05 --- /dev/null +++ b/lib/features/composer/presentation/model/save_to_draft_view_event.dart @@ -0,0 +1,39 @@ +import 'package:core/presentation/state/success.dart'; +import 'package:flutter/widgets.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/user/user_profile.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; + +class SaveToDraftViewEvent extends ViewEvent { + final BuildContext context; + final Session session; + final AccountId accountId; + final UserProfile userProfile; + final MailboxId draftMailboxId; + final EmailId? emailIdEditing; + final ComposerArguments? arguments; + + SaveToDraftViewEvent({ + required this.context, + required this.session, + required this.accountId, + required this.userProfile, + required this.draftMailboxId, + this.emailIdEditing, + this.arguments, + }); + + @override + List get props => [ + context, + session, + accountId, + userProfile, + draftMailboxId, + emailIdEditing, + arguments, + ]; +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/suggestion_email_address.dart b/lib/features/composer/presentation/model/suggestion_email_address.dart similarity index 100% rename from lib/features/composer/presentation/widgets/suggestion_email_address.dart rename to lib/features/composer/presentation/model/suggestion_email_address.dart diff --git a/lib/features/composer/presentation/styles/app_bar_composer_widget_style.dart b/lib/features/composer/presentation/styles/app_bar_composer_widget_style.dart new file mode 100644 index 0000000000..6569eb6063 --- /dev/null +++ b/lib/features/composer/presentation/styles/app_bar_composer_widget_style.dart @@ -0,0 +1,15 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AppBarComposerWidgetStyle { + static const double height = 52; + static const double iconSize = 20; + static const double space = 8; + + static const Color backgroundColor = AppColor.colorComposerAppBar; + static const Color iconColor = Colors.black; + + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 24); + static const EdgeInsetsGeometry iconPadding = EdgeInsetsDirectional.all(3); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/attachment_header_composer_widget_style.dart b/lib/features/composer/presentation/styles/attachment_header_composer_widget_style.dart new file mode 100644 index 0000000000..7da4e76673 --- /dev/null +++ b/lib/features/composer/presentation/styles/attachment_header_composer_widget_style.dart @@ -0,0 +1,26 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AttachmentHeaderComposerWidgetStyle { + static const double space = 8; + static const double iconSize = 20; + static const double sizeLabelRadius = 12; + + static const Color iconColor = AppColor.colorLabelComposer; + static const Color sizeLabelBackground = AppColor.primaryColor; + static const Color borderColor = AppColor.colorLineComposer; + + static const EdgeInsetsGeometry sizeLabelPadding = EdgeInsetsDirectional.symmetric(horizontal: 5, vertical: 2); + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.all(8); + + static const TextStyle labelTextSize = TextStyle( + fontSize: 13, + color: AppColor.colorLabelComposer, + fontWeight: FontWeight.w500 + ); + static const TextStyle sizeLabelTextSize = TextStyle( + fontSize: 12, + color: Colors.white, + fontWeight: FontWeight.w500 + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/attachment_item_composer_widget_style.dart b/lib/features/composer/presentation/styles/attachment_item_composer_widget_style.dart new file mode 100644 index 0000000000..ced09eaaae --- /dev/null +++ b/lib/features/composer/presentation/styles/attachment_item_composer_widget_style.dart @@ -0,0 +1,35 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AttachmentItemComposerWidgetStyle { + static const double radius = 8; + static const double iconSize = 20; + static const double space = 8; + static const double deleteIconSize = 18; + static const double deleteIconRadius = 10; + static const double width = 260; + + static const Color borderColor = AppColor.colorAttachmentBorder; + static const Color backgroundColor = Colors.white; + static const Color deleteIconColor = AppColor.colorRichButtonComposer; + + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.all(8); + static const EdgeInsetsGeometry deleteIconPadding = EdgeInsetsDirectional.all(3); + static const EdgeInsetsGeometry progressLoadingPadding = EdgeInsetsDirectional.only(top: 8); + + static const TextStyle labelTextStyle = TextStyle( + fontSize: 14, + color: Colors.black, + fontWeight: FontWeight.w500 + ); + static const TextStyle dotsLabelTextStyle = TextStyle( + fontSize: 12, + color: Colors.black, + fontWeight: FontWeight.w500 + ); + static const TextStyle sizeLabelTextStyle = TextStyle( + fontSize: 11, + color: AppColor.colorLabelComposer, + fontWeight: FontWeight.w500 + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/attachment_progress_loading_composer_widget_style.dart b/lib/features/composer/presentation/styles/attachment_progress_loading_composer_widget_style.dart new file mode 100644 index 0000000000..09c91ed5a2 --- /dev/null +++ b/lib/features/composer/presentation/styles/attachment_progress_loading_composer_widget_style.dart @@ -0,0 +1,10 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AttachmentProgressLoadingComposerWidgetStyle { + static const double height = 2; + static const double radius = 1; + + static const Color backgroundColor = AppColor.colorProgressLoadingBackground; + static const Color progressColor = AppColor.primaryColor; +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/composer_style.dart b/lib/features/composer/presentation/styles/composer_style.dart new file mode 100644 index 0000000000..d88eca6c80 --- /dev/null +++ b/lib/features/composer/presentation/styles/composer_style.dart @@ -0,0 +1,165 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; + +class ComposerStyle { + static const double radius = 28; + static const double keyboardMaxHeight = 500; + static const double keyboardToolBarHeight = 200; + static const double popupMenuRadius = 8; + + static const Color borderColor = AppColor.colorLineComposer; + static const Color backgroundEditorColor = Colors.white; + static const Color richToolbarColor = Colors.white; + static const Color mobileBackgroundColor = Colors.white; + static const Color popupItemIconColor = AppColor.primaryColor; + + static const EdgeInsetsGeometry richToolbarPadding = EdgeInsetsDirectional.symmetric(horizontal: 24, vertical: 8); + static const EdgeInsetsGeometry desktopRecipientPadding = EdgeInsetsDirectional.only(end: 24); + static const EdgeInsetsGeometry desktopRecipientMargin = EdgeInsetsDirectional.only(start: 24); + static const EdgeInsetsGeometry desktopSubjectMargin = EdgeInsetsDirectional.only(start: 24); + static const EdgeInsetsGeometry desktopSubjectPadding = EdgeInsetsDirectional.only(end: 24, top: 12, bottom: 12); + static const EdgeInsetsGeometry desktopEditorPadding = EdgeInsetsDirectional.symmetric(horizontal: 20); + static const EdgeInsetsGeometry tabletRecipientPadding = EdgeInsetsDirectional.only(end: 24); + static const EdgeInsetsGeometry tabletRecipientMargin = EdgeInsetsDirectional.only(start: 24); + static const EdgeInsetsGeometry tabletSubjectMargin = EdgeInsetsDirectional.only(start: 24); + static const EdgeInsetsGeometry tabletSubjectPadding = EdgeInsetsDirectional.only(end: 24, top: 12, bottom: 12); + static const EdgeInsetsGeometry tabletEditorPadding = EdgeInsetsDirectional.symmetric(horizontal: 20); + static const EdgeInsetsGeometry mobileRecipientPadding = EdgeInsetsDirectional.only(end: 16); + static const EdgeInsetsGeometry mobileRecipientMargin = EdgeInsetsDirectional.only(start: 16); + static const EdgeInsetsGeometry mobileSubjectMargin = EdgeInsetsDirectional.only(start: 16); + static const EdgeInsetsGeometry mobileSubjectPadding = EdgeInsetsDirectional.only(end: 16, top: 12, bottom: 12); + static const EdgeInsetsGeometry mobileEditorPadding = EdgeInsetsDirectional.symmetric(horizontal: 12); + static const EdgeInsetsGeometry popupItemPadding = EdgeInsetsDirectional.symmetric(horizontal: 12); + static const EdgeInsetsGeometry insertImageLoadingBarPadding = EdgeInsetsDirectional.only(top: 12); + + static const TextStyle popupItemTextStyle = TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500 + ); + + static const List richToolbarShadow = [ + BoxShadow( + color: AppColor.colorShadowBgContentEmail, + blurRadius: 24 + ), + BoxShadow( + color: AppColor.colorShadowBgContentEmail, + blurRadius: 2 + ), + ]; + + static EdgeInsetsGeometry getAppBarPadding(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isPortraitMobile(context) || responsiveUtils.isLandscapeMobile(context)) { + return const EdgeInsetsDirectional.only(end: 8); + } else { + return const EdgeInsetsDirectional.only(start: 24, end: 32); + } + } + + static EdgeInsetsGeometry getFromAddressPadding(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isPortraitMobile(context) || responsiveUtils.isLandscapeMobile(context)) { + return const EdgeInsetsDirectional.symmetric(horizontal: 16, vertical: 12); + } else { + return const EdgeInsetsDirectional.symmetric(horizontal: 8, vertical: 12); + } + } + + static EdgeInsetsGeometry getSubjectPadding(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isPortraitMobile(context) || responsiveUtils.isLandscapeMobile(context)) { + return const EdgeInsetsDirectional.symmetric(horizontal: 16, vertical: 8); + } else { + return const EdgeInsetsDirectional.only(start: 8, top: 8, bottom: 8, end: 16); + } + } + + static EdgeInsetsGeometry getSubjectWebPadding(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isMobile(context)) { + return const EdgeInsetsDirectional.symmetric(horizontal: 16); + } else { + return const EdgeInsetsDirectional.only(start: 8, end: 16); + } + } + + static double getAppBarHeight(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isPortraitMobile(context) || responsiveUtils.isLandscapeMobile(context)) { + return 57; + } else { + return 65; + } + } + + static double getSpace(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isPortraitMobile(context) || responsiveUtils.isLandscapeMobile(context)) { + return 8; + } else { + return 12; + } + } + + static EdgeInsetsGeometry getAttachmentPadding(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isPortraitMobile(context) || responsiveUtils.isLandscapeMobile(context)) { + return const EdgeInsetsDirectional.symmetric(horizontal: 16); + } else { + return const EdgeInsetsDirectional.only(start: 88, end: 48, top: 8); + } + } + + static EdgeInsetsGeometry getEditorPadding(BuildContext context, ResponsiveUtils responsiveUtils) { + if (PlatformInfo.isWeb) { + if (responsiveUtils.isMobile(context)) { + return const EdgeInsetsDirectional.symmetric(horizontal: 6); + } else { + return const EdgeInsetsDirectional.only(start: 78, end: 38); + } + } else { + if (responsiveUtils.isPortraitMobile(context) || responsiveUtils.isLandscapeMobile(context)) { + return const EdgeInsetsDirectional.symmetric(horizontal: 16); + } else { + return const EdgeInsetsDirectional.only(start: 88, end: 48); + } + } + } + + static EdgeInsetsGeometry getMarginForTablet(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isPortraitTablet(context)) { + return const EdgeInsetsDirectional.all(24); + } else { + return const EdgeInsetsDirectional.symmetric(vertical: 24); + } + } + + static double getWidthForTablet(BuildContext context, ResponsiveUtils responsiveUtils) { + final currentWidth = responsiveUtils.getSizeScreenWidth(context); + if (responsiveUtils.isPortraitTablet(context)) { + return currentWidth; + } else { + return currentWidth * 0.7; + } + } + + static int getMaxItemRowListAttachment(BuildContext context, BoxConstraints constraints) { + if (constraints.maxWidth < ResponsiveUtils.minTabletWidth) { + return 2; + } else if (constraints.maxWidth < ResponsiveUtils.minTabletLargeWidth) { + return 4; + } else { + return 5; + } + } + + static double getMaxWidthItemListAttachment(BuildContext context, BoxConstraints constraints) { + return constraints.maxWidth / getMaxItemRowListAttachment(context, constraints); + } + + static double getMaxHeightEmailAddressWidget(BuildContext context, BoxConstraints constraints, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isDesktop(context)) { + return constraints.maxHeight > 0 ? constraints.maxHeight * 0.3 : 150.0; + } else { + return constraints.maxHeight > 0 ? constraints.maxHeight * 0.4 : 150.0; + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/draggable_recipient_tag_widget_style.dart b/lib/features/composer/presentation/styles/draggable_recipient_tag_widget_style.dart new file mode 100644 index 0000000000..2f3339b204 --- /dev/null +++ b/lib/features/composer/presentation/styles/draggable_recipient_tag_widget_style.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class DraggableRecipientTagWidgetStyle { + static const double radius = 10; + static const double avatarIconSize = 24; + static const double avatarLabelFontSize = 12; + + static const Color deleteIconColor = Colors.white; + static const Color backgroundColor = AppColor.primaryColor; + + static const EdgeInsetsGeometry padding = EdgeInsets.symmetric(horizontal: 6, vertical: 3); + static const EdgeInsetsGeometry labelPadding = EdgeInsets.symmetric(horizontal: 8); + + static const TextStyle labelTextStyle = TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.normal + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/minimize_composer_widget_style.dart b/lib/features/composer/presentation/styles/minimize_composer_widget_style.dart new file mode 100644 index 0000000000..a41d7e5d5d --- /dev/null +++ b/lib/features/composer/presentation/styles/minimize_composer_widget_style.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class MinimizeComposerWidgetStyle { + static const double radius = 24; + static const double elevation = 16; + static const double width = 500; + static const double height = 50; + static const double space = 8; + static const double iconSize = 20; + + static const Color backgroundColor = Colors.white; + static const Color iconColor = Colors.black; + + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 24); + static const EdgeInsetsGeometry iconPadding = EdgeInsetsDirectional.all(3); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart b/lib/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart new file mode 100644 index 0000000000..4c401ff6eb --- /dev/null +++ b/lib/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class MobileAttachmentComposerWidgetStyle { + static const double listItemSpace = 8; + static const int maxItemRow = 2; + static const double listItemHeight = 50; + + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 16); + static const EdgeInsetsGeometry itemMargin = EdgeInsetsDirectional.only(top: 8); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/mobile/mobile_container_view_style.dart b/lib/features/composer/presentation/styles/mobile/mobile_container_view_style.dart new file mode 100644 index 0000000000..7012513aee --- /dev/null +++ b/lib/features/composer/presentation/styles/mobile/mobile_container_view_style.dart @@ -0,0 +1,13 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; + +class MobileContainerViewStyle { + static const Color outSideBackgroundColor = Colors.white; + static const Color backgroundColor = Colors.white; + static final Color keyboardToolbarBackgroundColor = PlatformInfo.isIOS + ? AppColor.colorBackgroundKeyboard + : AppColor.colorBackgroundKeyboardAndroid; + + static const EdgeInsets keyboardToolbarPadding = EdgeInsets.only(bottom: 64); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/mobile/tablet_bottom_bar_composer_widget_style.dart b/lib/features/composer/presentation/styles/mobile/tablet_bottom_bar_composer_widget_style.dart new file mode 100644 index 0000000000..88fe07647e --- /dev/null +++ b/lib/features/composer/presentation/styles/mobile/tablet_bottom_bar_composer_widget_style.dart @@ -0,0 +1,25 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class TabletBottomBarComposerWidgetStyle { + static const double iconRadius = 8; + static const double space = 10; + static const double sendButtonSpace = 12; + static const double iconSize = 20; + static const double sendButtonRadius = 8; + static const double sendButtonIconSpace = 5; + + static const Color backgroundColor = Colors.white; + static const Color iconColor = AppColor.colorRichButtonComposer; + static const Color sendButtonBackgroundColor = AppColor.primaryColor; + + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 32, vertical: 12); + static const EdgeInsetsGeometry iconPadding = EdgeInsetsDirectional.all(5); + static const EdgeInsetsGeometry sendButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 24); + static const TextStyle sendButtonTextStyle = TextStyle( + fontWeight: FontWeight.w500, + fontSize: 15, + color: Colors.white, + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/mobile/tablet_container_view_style.dart b/lib/features/composer/presentation/styles/mobile/tablet_container_view_style.dart new file mode 100644 index 0000000000..19c480ab8b --- /dev/null +++ b/lib/features/composer/presentation/styles/mobile/tablet_container_view_style.dart @@ -0,0 +1,41 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; + +class TabletContainerViewStyle { + static const double radius = 28; + static const double elevation = 16; + + static const Color backgroundColor = Colors.white; + static const Color outSideBackgroundColor = Colors.black38; + static final Color keyboardToolbarBackgroundColor = PlatformInfo.isIOS + ? AppColor.colorBackgroundKeyboard + : AppColor.colorBackgroundKeyboardAndroid; + + static const EdgeInsets keyboardToolbarPadding = EdgeInsets.only(bottom: 64); + + static EdgeInsetsGeometry getMargin( + BuildContext context, + ResponsiveUtils responsiveUtils + ) { + if (responsiveUtils.isTablet(context)) { + return const EdgeInsetsDirectional.all(24); + } else { + return const EdgeInsetsDirectional.symmetric(vertical: 24); + } + } + + static double getWidth( + BuildContext context, + ResponsiveUtils responsiveUtils + ) { + final currentWidth = responsiveUtils.getSizeScreenWidth(context); + if (responsiveUtils.isTablet(context)) { + return currentWidth; + } else { + return currentWidth * 0.7; + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/mobile_app_bar_composer_widget_style.dart b/lib/features/composer/presentation/styles/mobile_app_bar_composer_widget_style.dart new file mode 100644 index 0000000000..9496250503 --- /dev/null +++ b/lib/features/composer/presentation/styles/mobile_app_bar_composer_widget_style.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class MobileAppBarComposerWidgetStyle { + static const double height = 56; + static const double iconSize = 24; + static const double space = 12; + static const double iconRadius = 8; + static const double sendButtonIconSize = 28; + static const double richTextIconSize = 28; + + static const Color backgroundColor = AppColor.colorComposerAppBar; + static const Color iconColor = AppColor.colorMobileRichButtonComposer; + static const Color selectedBackgroundColor = AppColor.colorSelected; + static const Color selectedIconColor = AppColor.primaryColor; + + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 12); + static const EdgeInsetsGeometry iconPadding = EdgeInsetsDirectional.all(3); + static const EdgeInsetsGeometry richTextIconPadding = EdgeInsetsDirectional.all(2); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/recipient_composer_widget_style.dart b/lib/features/composer/presentation/styles/recipient_composer_widget_style.dart new file mode 100644 index 0000000000..84910facae --- /dev/null +++ b/lib/features/composer/presentation/styles/recipient_composer_widget_style.dart @@ -0,0 +1,44 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class RecipientComposerWidgetStyle { + static const double deleteRecipientFieldIconSize = 20; + static const double space = 8; + static const double enableBorderRadius = 10; + static const double suggestionsBoxElevation = 20.0; + static const double suggestionsBoxRadius = 20; + static const double suggestionsBoxMaxHeight = 350; + static const double suggestionBoxWidth = 300; + static const double minTextFieldWidth = 20; + static const double tagSpacing = 8; + + static const Duration suggestionDebounceDuration = Duration(milliseconds: 150); + + static const Color borderColor = AppColor.colorLineComposer; + static const Color deleteRecipientFieldIconColor = AppColor.colorCollapseMailbox; + static const Color enableBorderColor = AppColor.primaryColor; + static const Color suggestionsBoxBackgroundColor = Colors.white; + + static const EdgeInsetsGeometry deleteRecipientFieldIconPadding = EdgeInsetsDirectional.all(3); + static const EdgeInsetsGeometry prefixButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 3, horizontal: 5); + static const EdgeInsetsGeometry labelMargin = EdgeInsetsDirectional.only(top: 16); + static const EdgeInsetsGeometry recipientMargin = EdgeInsetsDirectional.only(top: 12); + + static const TextStyle prefixButtonTextStyle = TextStyle( + fontSize: 15, + fontWeight: FontWeight.normal, + decoration: TextDecoration.underline, + color: AppColor.colorPrefixButtonComposer + ); + static const TextStyle labelTextStyle = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColor.colorLabelComposer + ); + static const TextStyle inputTextStyle = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/recipient_suggestion_item_widget_style.dart b/lib/features/composer/presentation/styles/recipient_suggestion_item_widget_style.dart new file mode 100644 index 0000000000..2e43758649 --- /dev/null +++ b/lib/features/composer/presentation/styles/recipient_suggestion_item_widget_style.dart @@ -0,0 +1,22 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class RecipientSuggestionItemWidgetStyle { + static const double radius = 20; + static const double selectedIconSize = 24; + + static const EdgeInsetsGeometry suggestionDuplicatedMargin = EdgeInsets.all(8.0); + static const EdgeInsetsGeometry labelDuplicatedPadding = EdgeInsets.symmetric(horizontal: 8.0); + static const EdgeInsetsGeometry labelPadding = EdgeInsets.symmetric(horizontal: 16.0); + + static const TextStyle labelTextStyle = TextStyle( + color: AppColor.colorHintSearchBar, + fontSize: 13, + fontWeight: FontWeight.normal + ); + static const TextStyle labelHighlightTextStyle = TextStyle( + color: Colors.black, + fontSize: 13, + fontWeight: FontWeight.bold + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/recipient_tag_item_widget_style.dart b/lib/features/composer/presentation/styles/recipient_tag_item_widget_style.dart new file mode 100644 index 0000000000..1585ada196 --- /dev/null +++ b/lib/features/composer/presentation/styles/recipient_tag_item_widget_style.dart @@ -0,0 +1,19 @@ +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; + +class RecipientTagItemWidgetStyle { + static const double radius = 10; + static const double avatarIconSize = 24; + static const double avatarLabelFontSize = 12; + + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.only(start: 4); + static const EdgeInsetsGeometry counterPadding = EdgeInsetsDirectional.symmetric(vertical: 5, horizontal: 8); + static const EdgeInsetsGeometry mobileCounterPadding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 8); + static const EdgeInsetsGeometry counterMargin = EdgeInsetsDirectional.only(top: PlatformInfo.isWeb ? 8 : 0); + + static const TextStyle labelTextStyle = TextStyle( + color: Colors.black, + fontSize: 17, + fontWeight: FontWeight.w400 + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/subject_composer_widget_style.dart b/lib/features/composer/presentation/styles/subject_composer_widget_style.dart new file mode 100644 index 0000000000..8fb27e85d6 --- /dev/null +++ b/lib/features/composer/presentation/styles/subject_composer_widget_style.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class SubjectComposerWidgetStyle { + static const double space = 12; + + static const Color cursorColor = AppColor.primaryColor; + static const Color borderColor = AppColor.colorLineComposer; + + static const TextStyle labelTextStyle = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: AppColor.colorLabelComposer + ); + static const TextStyle inputTextStyle = TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500 + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/title_composer_widget_style.dart b/lib/features/composer/presentation/styles/title_composer_widget_style.dart new file mode 100644 index 0000000000..9dc8c1841d --- /dev/null +++ b/lib/features/composer/presentation/styles/title_composer_widget_style.dart @@ -0,0 +1,10 @@ + +import 'package:flutter/material.dart'; + +class TitleComposerWidgetStyle { + static const TextStyle textStyle = TextStyle( + fontSize: 17, + fontWeight: FontWeight.w600, + color: Colors.black + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/web/attachment_composer_widget_style.dart b/lib/features/composer/presentation/styles/web/attachment_composer_widget_style.dart new file mode 100644 index 0000000000..6f41288b26 --- /dev/null +++ b/lib/features/composer/presentation/styles/web/attachment_composer_widget_style.dart @@ -0,0 +1,22 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AttachmentComposerWidgetStyle { + static const double maxHeight = 150; + static const double listItemSpace = 8; + + static const Color backgroundColor = Colors.white; + + static const EdgeInsetsGeometry listItemPadding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 16); + + static const List shadow = [ + BoxShadow( + color: AppColor.colorShadowBgContentEmail, + blurRadius: 24 + ), + BoxShadow( + color: AppColor.colorShadowBgContentEmail, + blurRadius: 2 + ), + ]; +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart b/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart new file mode 100644 index 0000000000..3389db408a --- /dev/null +++ b/lib/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart @@ -0,0 +1,29 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class BottomBarComposerWidgetStyle { + static const double iconRadius = 8; + static const double space = 10; + static const double sendButtonSpace = 12; + static const double iconSize = 20; + static const double richTextIconSize = 24; + static const double sendButtonRadius = 8; + static const double sendButtonIconSpace = 5; + + static const Color backgroundColor = Colors.white; + static const Color iconColor = AppColor.colorRichButtonComposer; + static const Color sendButtonBackgroundColor = AppColor.primaryColor; + static const Color selectedBackgroundColor = AppColor.colorSelected; + static const Color selectedIconColor = AppColor.primaryColor; + + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(horizontal: 32, vertical: 12); + static const EdgeInsetsGeometry iconPadding = EdgeInsetsDirectional.all(5); + static const EdgeInsetsGeometry sendButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 24); + static const EdgeInsetsGeometry richTextIconPadding = EdgeInsetsDirectional.all(2); + static const TextStyle sendButtonTextStyle = TextStyle( + fontWeight: FontWeight.w500, + fontSize: 15, + color: Colors.white, + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/web/desktop_responsive_container_view_style.dart b/lib/features/composer/presentation/styles/web/desktop_responsive_container_view_style.dart new file mode 100644 index 0000000000..c0bec80eea --- /dev/null +++ b/lib/features/composer/presentation/styles/web/desktop_responsive_container_view_style.dart @@ -0,0 +1,11 @@ + +import 'package:flutter/material.dart'; + +class DesktopResponsiveContainerViewStyle { + static const double radius = 28; + static const double margin = 20; + static const double elevation = 16; + + static const Color backgroundColor = Colors.white; + static const Color outSideBackgroundColor = Colors.black38; +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart b/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart new file mode 100644 index 0000000000..033c224f68 --- /dev/null +++ b/lib/features/composer/presentation/styles/web/drop_zone_widget_style.dart @@ -0,0 +1,22 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class DropZoneWidgetStyle { + static const double space = 20; + static const double borderWidth = 2; + static const double radius = 16; + + static const List dashSize = [6, 3]; + + static const Color backgroundColor = AppColor.colorDropZoneBackground; + static const Color borderColor = AppColor.colorDropZoneBorder; + + static const EdgeInsetsGeometry padding = EdgeInsets.all(20); + static const EdgeInsetsGeometry margin = EdgeInsetsDirectional.symmetric(vertical: 8); + + static const TextStyle labelTextStyle = TextStyle( + color: Colors.black, + fontSize: 22, + fontWeight: FontWeight.w600 + ); +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/web/mobile_responsive_container_view_style.dart b/lib/features/composer/presentation/styles/web/mobile_responsive_container_view_style.dart new file mode 100644 index 0000000000..fcca155ecc --- /dev/null +++ b/lib/features/composer/presentation/styles/web/mobile_responsive_container_view_style.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +class MobileResponsiveContainerViewStyle { + static const double radius = 28; + static const double elevation = 16; + + static const Color outSideBackgroundColor = Colors.white; +} \ No newline at end of file diff --git a/lib/features/composer/presentation/styles/web/tablet_responsive_container_view_style.dart b/lib/features/composer/presentation/styles/web/tablet_responsive_container_view_style.dart new file mode 100644 index 0000000000..8f7645d7af --- /dev/null +++ b/lib/features/composer/presentation/styles/web/tablet_responsive_container_view_style.dart @@ -0,0 +1,34 @@ + +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; + +class TabletResponsiveContainerViewStyle { + static const double radius = 28; + static const double elevation = 16; + + static const Color backgroundColor = Colors.white; + static const Color outSideBackgroundColor = Colors.black38; + + static EdgeInsetsGeometry getMargin( + BuildContext context, + ResponsiveUtils responsiveUtils + ) { + if (responsiveUtils.isTablet(context)) { + return const EdgeInsetsDirectional.all(24); + } else { + return const EdgeInsetsDirectional.symmetric(vertical: 24); + } + } + + static double getWidth( + BuildContext context, + ResponsiveUtils responsiveUtils + ) { + final currentWidth = responsiveUtils.getSizeScreenWidth(context); + if (responsiveUtils.isTablet(context)) { + return currentWidth; + } else { + return currentWidth * 0.7; + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/view/editor_view_mixin.dart b/lib/features/composer/presentation/view/editor_view_mixin.dart new file mode 100644 index 0000000000..8e54f1cb01 --- /dev/null +++ b/lib/features/composer/presentation/view/editor_view_mixin.dart @@ -0,0 +1,27 @@ + +import 'package:core/presentation/extensions/html_extension.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; + +mixin EditorViewMixin { + String getEmailContentQuotedAsHtml({ + required BuildContext context, + required String emailContent, + required EmailActionType emailActionType, + required PresentationEmail presentationEmail, + }) { + final headerEmailQuoted = emailActionType.getHeaderEmailQuoted( + context: context, + presentationEmail: presentationEmail + ); + log('EditorViewMixin::getEmailContentQuotedAsHtml:headerEmailQuoted: $headerEmailQuoted'); + final headerEmailQuotedAsHtml = headerEmailQuoted != null + ? headerEmailQuoted.addCiteTag() + : ''; + final emailQuotedHtml = '${HtmlExtension.editorStartTags}$headerEmailQuotedAsHtml${emailContent.addBlockQuoteTag()}'; + return emailQuotedHtml; + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/view/mobile/mobile_container_view.dart b/lib/features/composer/presentation/view/mobile/mobile_container_view.dart new file mode 100644 index 0000000000..3ba955d0da --- /dev/null +++ b/lib/features/composer/presentation/view/mobile/mobile_container_view.dart @@ -0,0 +1,76 @@ +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:get/get.dart'; +import 'package:rich_text_composer/rich_text_composer.dart' as rich_composer; +import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/mobile/mobile_container_view_style.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnInsertImageAction = Function(BoxConstraints constraints); + +class MobileContainerView extends StatelessWidget { + + final Widget Function(BuildContext context) childBuilder; + final rich_composer.RichTextController keyboardRichTextController; + final VoidCallback onCloseViewAction; + final VoidCallback? onAttachFileAction; + final OnInsertImageAction? onInsertImageAction; + final VoidCallback? onClearFocusAction; + final Color? backgroundColor; + + final _responsiveUtils = Get.find(); + + MobileContainerView({ + super.key, + required this.childBuilder, + required this.keyboardRichTextController, + required this.onCloseViewAction, + this.onAttachFileAction, + this.onInsertImageAction, + this.onClearFocusAction, + this.backgroundColor, + }); + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + onCloseViewAction.call(); + return true; + }, + child: GestureDetector( + onTap: onClearFocusAction, + child: Scaffold( + backgroundColor: backgroundColor ?? MobileContainerViewStyle.outSideBackgroundColor, + resizeToAvoidBottomInset: false, + body: LayoutBuilder(builder: (context, constraints) { + return KeyboardVisibilityBuilder(builder: (context, isKeyboardVisible) { + return rich_composer.KeyboardRichText( + richTextController: keyboardRichTextController, + keyBroadToolbar: RichTextKeyboardToolBar( + backgroundKeyboardToolBarColor: MobileContainerViewStyle.keyboardToolbarBackgroundColor, + isLandScapeMode: _responsiveUtils.isLandscapeMobile(context), + insertAttachment: onAttachFileAction, + insertImage: () => onInsertImageAction != null + ? onInsertImageAction!(constraints) + : null, + richTextController: keyboardRichTextController, + titleQuickStyleBottomSheet: AppLocalizations.of(context).titleQuickStyles, + titleBackgroundBottomSheet: AppLocalizations.of(context).titleBackground, + titleForegroundBottomSheet: AppLocalizations.of(context).titleForeground, + titleFormatBottomSheet: AppLocalizations.of(context).titleFormat, + titleBack: AppLocalizations.of(context).format, + ), + paddingChild: isKeyboardVisible + ? MobileContainerViewStyle.keyboardToolbarPadding + : EdgeInsets.zero, + child: childBuilder(context), + ); + }); + }) + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart b/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart new file mode 100644 index 0000000000..da680722cb --- /dev/null +++ b/lib/features/composer/presentation/view/mobile/mobile_editor_view.dart @@ -0,0 +1,130 @@ +import 'package:core/presentation/extensions/html_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:tmail_ui_user/features/base/widget/cupertino_loading_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/editor_view_mixin.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; + +class MobileEditorView extends StatelessWidget with EditorViewMixin { + + final ComposerArguments? arguments; + final Either? contentViewState; + final OnCreatedEditorAction onCreatedEditorAction; + final OnLoadCompletedEditorAction onLoadCompletedEditorAction; + + const MobileEditorView({ + super.key, + required this.onCreatedEditorAction, + required this.onLoadCompletedEditorAction, + this.arguments, + this.contentViewState, + }); + + @override + Widget build(BuildContext context) { + if (arguments == null) { + return const SizedBox.shrink(); + } + + switch (arguments!.emailActionType) { + case EmailActionType.compose: + case EmailActionType.composeFromEmailAddress: + case EmailActionType.composeFromFileShared: + return MobileEditorWidget( + content: HtmlExtension.editorStartTags, + direction: AppUtils.getCurrentDirection(context), + onCreatedEditorAction: onCreatedEditorAction, + onLoadCompletedEditorAction: onLoadCompletedEditorAction + ); + case EmailActionType.editDraft: + case EmailActionType.editSendingEmail: + case EmailActionType.composeFromContentShared: + case EmailActionType.reopenComposerBrowser: + if (contentViewState == null) { + return const SizedBox.shrink(); + } + return contentViewState!.fold( + (failure) => MobileEditorWidget( + content: HtmlExtension.editorStartTags, + direction: AppUtils.getCurrentDirection(context), + onCreatedEditorAction: onCreatedEditorAction, + onLoadCompletedEditorAction: onLoadCompletedEditorAction + ), + (success) { + if (success is GetEmailContentLoading) { + return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); + } else { + var newContent = success is GetEmailContentSuccess + ? success.htmlEmailContent + : HtmlExtension.editorStartTags; + if (newContent.isEmpty) { + newContent = HtmlExtension.editorStartTags; + } + return MobileEditorWidget( + content: newContent, + direction: AppUtils.getCurrentDirection(context), + onCreatedEditorAction: onCreatedEditorAction, + onLoadCompletedEditorAction: onLoadCompletedEditorAction + ); + } + } + ); + case EmailActionType.reply: + case EmailActionType.replyAll: + case EmailActionType.forward: + if (contentViewState == null) { + return const SizedBox.shrink(); + } + return contentViewState!.fold( + (failure) { + final emailContentQuoted = getEmailContentQuotedAsHtml( + context: context, + emailContent: '', + emailActionType: arguments!.emailActionType, + presentationEmail: arguments!.presentationEmail! + ); + return MobileEditorWidget( + content: emailContentQuoted, + direction: AppUtils.getCurrentDirection(context), + onCreatedEditorAction: onCreatedEditorAction, + onLoadCompletedEditorAction: onLoadCompletedEditorAction + ); + }, + (success) { + if (success is TransformHtmlEmailContentLoading) { + return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); + } else { + final emailContentQuoted = getEmailContentQuotedAsHtml( + context: context, + emailContent: success is TransformHtmlEmailContentSuccess + ? success.htmlContent + : '', + emailActionType: arguments!.emailActionType, + presentationEmail: arguments!.presentationEmail! + ); + return MobileEditorWidget( + content: emailContentQuoted, + direction: AppUtils.getCurrentDirection(context), + onCreatedEditorAction: onCreatedEditorAction, + onLoadCompletedEditorAction: onLoadCompletedEditorAction + ); + } + } + ); + default: + return MobileEditorWidget( + content: HtmlExtension.editorStartTags, + direction: AppUtils.getCurrentDirection(context), + onCreatedEditorAction: onCreatedEditorAction, + onLoadCompletedEditorAction: onLoadCompletedEditorAction + ); + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/view/mobile/tablet_container_view.dart b/lib/features/composer/presentation/view/mobile/tablet_container_view.dart new file mode 100644 index 0000000000..95b1f78f50 --- /dev/null +++ b/lib/features/composer/presentation/view/mobile/tablet_container_view.dart @@ -0,0 +1,86 @@ +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; +import 'package:get/get.dart'; +import 'package:rich_text_composer/rich_text_composer.dart'; +import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/mobile/tablet_container_view_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/mobile/mobile_container_view.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class TabletContainerView extends StatelessWidget { + + final Widget Function(BuildContext context, BoxConstraints constraints) childBuilder; + final RichTextController keyboardRichTextController; + final VoidCallback onCloseViewAction; + final VoidCallback? onAttachFileAction; + final OnInsertImageAction? onInsertImageAction; + final VoidCallback? onClearFocusAction; + + final _responsiveUtils = Get.find(); + + TabletContainerView({ + super.key, + required this.childBuilder, + required this.keyboardRichTextController, + required this.onCloseViewAction, + this.onAttachFileAction, + this.onInsertImageAction, + this.onClearFocusAction, + }); + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () async { + onCloseViewAction.call(); + return true; + }, + child: GestureDetector( + onTap: onClearFocusAction, + child: Scaffold( + backgroundColor: TabletContainerViewStyle.outSideBackgroundColor, + body: LayoutBuilder(builder: (context, constraints) { + return KeyboardVisibilityBuilder(builder: (context, isKeyboardVisible) { + return KeyboardRichText( + richTextController: keyboardRichTextController, + keyBroadToolbar: RichTextKeyboardToolBar( + backgroundKeyboardToolBarColor: TabletContainerViewStyle.keyboardToolbarBackgroundColor, + isLandScapeMode: _responsiveUtils.isLandscapeMobile(context), + insertAttachment: onAttachFileAction, + insertImage: () => onInsertImageAction != null + ? onInsertImageAction!(constraints) + : null, + richTextController: keyboardRichTextController, + titleQuickStyleBottomSheet: AppLocalizations.of(context).titleQuickStyles, + titleBackgroundBottomSheet: AppLocalizations.of(context).titleBackground, + titleForegroundBottomSheet: AppLocalizations.of(context).titleForeground, + titleFormatBottomSheet: AppLocalizations.of(context).titleFormat, + titleBack: AppLocalizations.of(context).format, + ), + paddingChild: isKeyboardVisible + ? TabletContainerViewStyle.keyboardToolbarPadding + : EdgeInsets.zero, + child: Center( + child: Card( + elevation: TabletContainerViewStyle.elevation, + margin: TabletContainerViewStyle.getMargin(context, _responsiveUtils), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(TabletContainerViewStyle.radius)) + ), + clipBehavior: Clip.antiAlias, + child: Container( + color: TabletContainerViewStyle.backgroundColor, + width: TabletContainerViewStyle.getWidth(context, _responsiveUtils), + child: childBuilder.call(context, constraints) + ), + ), + ), + ); + }); + }) + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/view/web/desktop_responsive_container_view.dart b/lib/features/composer/presentation/view/web/desktop_responsive_container_view.dart new file mode 100644 index 0000000000..4b32e2d5ff --- /dev/null +++ b/lib/features/composer/presentation/view/web/desktop_responsive_container_view.dart @@ -0,0 +1,97 @@ +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/desktop_responsive_container_view_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/minimize_composer_widget.dart'; + +class DesktopResponsiveContainerView extends StatelessWidget { + + final ScreenDisplayMode displayMode; + final String emailSubject; + final VoidCallback onCloseViewAction; + final OnChangeDisplayModeAction onChangeDisplayModeAction; + final Widget Function(BuildContext context, BoxConstraints constraints) childBuilder; + + final _responsiveUtils = Get.find(); + + DesktopResponsiveContainerView({ + super.key, + required this.childBuilder, + required this.displayMode, + required this.emailSubject, + required this.onCloseViewAction, + required this.onChangeDisplayModeAction, + }); + + @override + Widget build(BuildContext context) { + if (displayMode == ScreenDisplayMode.minimize) { + return PositionedDirectional( + end: DesktopResponsiveContainerViewStyle.margin, + bottom: DesktopResponsiveContainerViewStyle.margin, + child: PointerInterceptor( + child: MinimizeComposerWidget( + emailSubject: emailSubject, + onCloseViewAction: onCloseViewAction, + onChangeDisplayModeAction: onChangeDisplayModeAction, + ), + ) + ); + } else if (displayMode == ScreenDisplayMode.normal) { + final maxWidth = _responsiveUtils.getSizeScreenWidth(context) * 0.5; + final maxHeight = _responsiveUtils.getSizeScreenHeight(context) * 0.75; + + return PositionedDirectional( + end: DesktopResponsiveContainerViewStyle.margin, + bottom: DesktopResponsiveContainerViewStyle.margin, + child: Card( + elevation: DesktopResponsiveContainerViewStyle.elevation, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(DesktopResponsiveContainerViewStyle.radius)) + ), + clipBehavior: Clip.antiAlias, + child: Container( + color: DesktopResponsiveContainerViewStyle.backgroundColor, + width: maxWidth, + height: maxHeight, + child: LayoutBuilder(builder: (context, constraints) => + PointerInterceptor( + child: childBuilder.call(context, constraints) + ) + ) + ), + ) + ); + } else if (displayMode == ScreenDisplayMode.fullScreen) { + final maxWidth = _responsiveUtils.getSizeScreenWidth(context) * 0.85; + final maxHeight = _responsiveUtils.getSizeScreenHeight(context) * 0.9; + + return Scaffold( + backgroundColor: DesktopResponsiveContainerViewStyle.outSideBackgroundColor, + body: Center( + child: Card( + elevation: DesktopResponsiveContainerViewStyle.elevation, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(DesktopResponsiveContainerViewStyle.radius)) + ), + clipBehavior: Clip.antiAlias, + child: Container( + color: DesktopResponsiveContainerViewStyle.backgroundColor, + width: maxWidth, + height: maxHeight, + child: LayoutBuilder(builder: (context, constraints) => + PointerInterceptor( + child: childBuilder.call(context, constraints) + ) + ) + ), + ), + ) + ); + } else { + return const SizedBox.shrink(); + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/view/web/mobile_responsive_container_view.dart b/lib/features/composer/presentation/view/web/mobile_responsive_container_view.dart new file mode 100644 index 0000000000..0efbe85134 --- /dev/null +++ b/lib/features/composer/presentation/view/web/mobile_responsive_container_view.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/mobile_responsive_container_view_style.dart'; + +class MobileResponsiveContainerView extends StatelessWidget { + + final Widget Function(BuildContext context, BoxConstraints constraints) childBuilder; + + const MobileResponsiveContainerView({ + super.key, + required this.childBuilder, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: MobileResponsiveContainerViewStyle.outSideBackgroundColor, + body: LayoutBuilder(builder: (context, constraints) => + PointerInterceptor( + child: childBuilder.call(context, constraints) + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/view/web/tablet_responsive_container_view.dart b/lib/features/composer/presentation/view/web/tablet_responsive_container_view.dart new file mode 100644 index 0000000000..1263612d76 --- /dev/null +++ b/lib/features/composer/presentation/view/web/tablet_responsive_container_view.dart @@ -0,0 +1,43 @@ +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/tablet_responsive_container_view_style.dart'; + +class TabletResponsiveContainerView extends StatelessWidget { + + final Widget Function(BuildContext context, BoxConstraints constraints) childBuilder; + + final _responsiveUtils = Get.find(); + + TabletResponsiveContainerView({ + super.key, + required this.childBuilder, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: TabletResponsiveContainerViewStyle.outSideBackgroundColor, + body: Center( + child: Card( + elevation: TabletResponsiveContainerViewStyle.elevation, + margin: TabletResponsiveContainerViewStyle.getMargin(context, _responsiveUtils), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(TabletResponsiveContainerViewStyle.radius)) + ), + clipBehavior: Clip.antiAlias, + child: Container( + color: TabletResponsiveContainerViewStyle.backgroundColor, + width: TabletResponsiveContainerViewStyle.getWidth(context, _responsiveUtils), + child: LayoutBuilder(builder: (context, constraints) => + PointerInterceptor( + child: childBuilder.call(context, constraints) + ) + ) + ), + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/view/web/web_editor_view.dart b/lib/features/composer/presentation/view/web/web_editor_view.dart new file mode 100644 index 0000000000..c8fa71d015 --- /dev/null +++ b/lib/features/composer/presentation/view/web/web_editor_view.dart @@ -0,0 +1,206 @@ + +import 'package:core/presentation/extensions/html_extension.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:html_editor_enhanced/html_editor.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:tmail_ui_user/features/base/widget/cupertino_loading_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/view/editor_view_mixin.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/web_editor_widget.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; + +class WebEditorView extends StatelessWidget with EditorViewMixin { + + final HtmlEditorController editorController; + final ComposerArguments? arguments; + final Either? contentViewState; + final String? currentWebContent; + final OnInitialContentEditorAction? onInitial; + final OnChangeContentEditorAction? onChangeContent; + final VoidCallback? onFocus; + final VoidCallback? onUnFocus; + final OnMouseDownEditorAction? onMouseDown; + final OnEditorSettingsChange? onEditorSettings; + final OnImageUploadSuccessAction? onImageUploadSuccessAction; + final OnImageUploadFailureAction? onImageUploadFailureAction; + final double? width; + final double? height; + + const WebEditorView({ + super.key, + required this.editorController, + this.arguments, + this.contentViewState, + this.currentWebContent, + this.onInitial, + this.onChangeContent, + this.onFocus, + this.onUnFocus, + this.onMouseDown, + this.onEditorSettings, + this.onImageUploadSuccessAction, + this.onImageUploadFailureAction, + this.width, + this.height, + }); + + @override + Widget build(BuildContext context) { + if (arguments == null) { + return const SizedBox.shrink(); + } + + switch(arguments!.emailActionType) { + case EmailActionType.compose: + case EmailActionType.composeFromEmailAddress: + case EmailActionType.composeFromFileShared: + return WebEditorWidget( + editorController: editorController, + content: currentWebContent ?? HtmlExtension.editorStartTags, + direction: AppUtils.getCurrentDirection(context), + onInitial: onInitial, + onChangeContent: onChangeContent, + onFocus: onFocus, + onUnFocus: onUnFocus, + onMouseDown: onMouseDown, + onEditorSettings: onEditorSettings, + onImageUploadSuccessAction: onImageUploadSuccessAction, + onImageUploadFailureAction: onImageUploadFailureAction, + width: width, + height: height, + ); + case EmailActionType.editDraft: + case EmailActionType.editSendingEmail: + case EmailActionType.composeFromContentShared: + case EmailActionType.reopenComposerBrowser: + if (contentViewState == null) { + return const SizedBox.shrink(); + } + return contentViewState!.fold( + (failure) => WebEditorWidget( + editorController: editorController, + content: currentWebContent ?? HtmlExtension.editorStartTags, + direction: AppUtils.getCurrentDirection(context), + onInitial: onInitial, + onChangeContent: onChangeContent, + onFocus: onFocus, + onUnFocus: onUnFocus, + onMouseDown: onMouseDown, + onEditorSettings: onEditorSettings, + onImageUploadSuccessAction: onImageUploadSuccessAction, + onImageUploadFailureAction: onImageUploadFailureAction, + width: width, + height: height, + ), + (success) { + if (success is GetEmailContentLoading) { + return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); + } else { + var newContent = success is GetEmailContentSuccess + ? success.htmlEmailContent + : HtmlExtension.editorStartTags; + if (newContent.isEmpty) { + newContent = HtmlExtension.editorStartTags; + } + return WebEditorWidget( + editorController: editorController, + content: currentWebContent ?? newContent, + direction: AppUtils.getCurrentDirection(context), + onInitial: onInitial, + onChangeContent: onChangeContent, + onFocus: onFocus, + onUnFocus: onUnFocus, + onMouseDown: onMouseDown, + onEditorSettings: onEditorSettings, + onImageUploadSuccessAction: onImageUploadSuccessAction, + onImageUploadFailureAction: onImageUploadFailureAction, + width: width, + height: height, + ); + } + } + ); + case EmailActionType.reply: + case EmailActionType.replyAll: + case EmailActionType.forward: + if (contentViewState == null) { + return const SizedBox.shrink(); + } + return contentViewState!.fold( + (failure) { + final emailContentQuoted = getEmailContentQuotedAsHtml( + context: context, + emailContent: '', + emailActionType: arguments!.emailActionType, + presentationEmail: arguments!.presentationEmail! + ); + return WebEditorWidget( + editorController: editorController, + content: currentWebContent ?? emailContentQuoted, + direction: AppUtils.getCurrentDirection(context), + onInitial: onInitial, + onChangeContent: onChangeContent, + onFocus: onFocus, + onUnFocus: onUnFocus, + onMouseDown: onMouseDown, + onEditorSettings: onEditorSettings, + onImageUploadSuccessAction: onImageUploadSuccessAction, + onImageUploadFailureAction: onImageUploadFailureAction, + width: width, + height: height, + ); + }, + (success) { + if (success is TransformHtmlEmailContentLoading) { + return const CupertinoLoadingWidget(padding: EdgeInsets.all(16.0)); + } else { + final emailContentQuoted = getEmailContentQuotedAsHtml( + context: context, + emailContent: success is TransformHtmlEmailContentSuccess + ? success.htmlContent + : '', + emailActionType: arguments!.emailActionType, + presentationEmail: arguments!.presentationEmail! + ); + return WebEditorWidget( + editorController: editorController, + content: currentWebContent ?? emailContentQuoted, + direction: AppUtils.getCurrentDirection(context), + onInitial: onInitial, + onChangeContent: onChangeContent, + onFocus: onFocus, + onUnFocus: onUnFocus, + onMouseDown: onMouseDown, + onEditorSettings: onEditorSettings, + onImageUploadSuccessAction: onImageUploadSuccessAction, + onImageUploadFailureAction: onImageUploadFailureAction, + width: width, + height: height, + ); + } + } + ); + default: + return WebEditorWidget( + editorController: editorController, + content: currentWebContent ?? HtmlExtension.editorStartTags, + direction: AppUtils.getCurrentDirection(context), + onInitial: onInitial, + onChangeContent: onChangeContent, + onFocus: onFocus, + onUnFocus: onUnFocus, + onMouseDown: onMouseDown, + onEditorSettings: onEditorSettings, + onImageUploadSuccessAction: onImageUploadSuccessAction, + onImageUploadFailureAction: onImageUploadFailureAction, + width: width, + height: height, + ); + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/attachment_file_composer_builder.dart b/lib/features/composer/presentation/widgets/attachment_file_composer_builder.dart deleted file mode 100644 index c09cc25477..0000000000 --- a/lib/features/composer/presentation/widgets/attachment_file_composer_builder.dart +++ /dev/null @@ -1,133 +0,0 @@ - -import 'package:core/core.dart'; -import 'package:filesize/filesize.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; -import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; -import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -typedef OnDeleteAttachmentAction = void Function(UploadFileState fileState); - -class AttachmentFileComposerBuilder extends StatelessWidget with AppLoaderMixin { - - final _imagePaths = Get.find(); - - final UploadFileState fileState; - final double? maxWidth; - final EdgeInsets? itemMargin; - final OnDeleteAttachmentAction? onDeleteAttachmentAction; - final Widget? buttonAction; - - AttachmentFileComposerBuilder(this.fileState, { - super.key, - this.maxWidth, - this.itemMargin, - this.buttonAction, - this.onDeleteAttachmentAction, - }); - - @override - Widget build(BuildContext context) { - return Theme( - data: ThemeData( - splashColor: Colors.transparent , - highlightColor: Colors.transparent), - child: Container( - margin: itemMargin ?? EdgeInsets.zero, - padding: EdgeInsets.zero, - alignment: Alignment.center, - width: maxWidth, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColor.colorInputBorderCreateMailbox), - color: Colors.white), - child: Stack(children: [ - ListTile( - contentPadding: EdgeInsets.zero, - focusColor: AppColor.primaryColor, - hoverColor: AppColor.primaryColor, - onTap: () {}, - leading: Padding( - padding: const EdgeInsets.only( - left: 8, - bottom: BuildUtils.isWeb ? 6 : 14), - child: SvgPicture.asset( - fileState.getIcon(_imagePaths), - width: 40, - height: 40, - fit: BoxFit.fill), - ), - title: Transform( - transform: Matrix4.translationValues( - BuildUtils.isWeb ? 0.0 : -8.0, - BuildUtils.isWeb ? -8.0 : -10.0, - 0.0), - child: Padding( - padding: const EdgeInsets.only(right: BuildUtils.isWeb ? 20 : 16), - child: Text( - fileState.fileName, - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: const TextStyle( - fontSize: 14, - color: Colors.black, - fontWeight: FontWeight.w500), - ), - ) - ), - subtitle: fileState.fileSize != 0 - ? Transform( - transform: Matrix4.translationValues( - BuildUtils.isWeb ? 0.0 : -8.0, - BuildUtils.isWeb ? -8.0 : -10.0, - 0.0), - child: Text( - filesize(fileState.fileSize), - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.normal, - color: AppColor.colorContentEmail))) - : null, - ), - Positioned( - right: BuildUtils.isWeb ? -5 : -12, - top: BuildUtils.isWeb ? -5 : -12, - child: buildIconWeb( - icon: SvgPicture.asset(_imagePaths.icDeleteAttachment, fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).delete, - onTap: () { - if (onDeleteAttachmentAction != null) { - onDeleteAttachmentAction!.call(fileState); - } - } - ) - ), - Align(alignment: Alignment.bottomCenter, child: _progressLoading), - ]), - ) - ); - } - - Widget get _progressLoading { - switch(fileState.uploadStatus) { - case UploadFileStatus.waiting: - return Padding( - padding: const EdgeInsets.only(left: 8, right: 8, top: 50), - child: horizontalLoadingWidget); - case UploadFileStatus.uploading: - return Padding( - padding: const EdgeInsets.only(top: 50), - child: horizontalPercentLoadingWidget(fileState.percentUploading)); - case UploadFileStatus.uploadFailed: - case UploadFileStatus.succeed: - return const SizedBox.shrink(); - } - } -} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart b/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart new file mode 100644 index 0000000000..ffc1070c66 --- /dev/null +++ b/lib/features/composer/presentation/widgets/attachment_header_composer_widget.dart @@ -0,0 +1,78 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/attachment_header_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/upload/presentation/extensions/list_upload_file_state_extension.dart'; +import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnToggleExpandAttachmentViewAction = Function(bool isCollapsed); + +class AttachmentHeaderComposerWidget extends StatelessWidget { + + final List listFileUploaded; + final bool isCollapsed; + final OnToggleExpandAttachmentViewAction onToggleExpandAction; + + final _imagePaths = Get.find(); + + AttachmentHeaderComposerWidget({ + super.key, + required this.listFileUploaded, + required this.isCollapsed, + required this.onToggleExpandAction, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () => onToggleExpandAction.call(isCollapsed), + child: Container( + padding: AttachmentHeaderComposerWidgetStyle.padding, + decoration: isCollapsed + ? null + : const BoxDecoration( + border: Border( + bottom: BorderSide( + color: AttachmentHeaderComposerWidgetStyle.borderColor, + width: 1 + ) + ) + ), + child: Row( + children: [ + TMailButtonWidget.fromIcon( + icon: isCollapsed + ? _imagePaths.icArrowRight + : _imagePaths.icArrowBottom, + iconSize: AttachmentHeaderComposerWidgetStyle.iconSize, + iconColor: AttachmentHeaderComposerWidgetStyle.iconColor, + backgroundColor: Colors.white, + padding: EdgeInsets.zero, + onTapActionCallback: () => onToggleExpandAction.call(isCollapsed), + ), + const SizedBox(width: AttachmentHeaderComposerWidgetStyle.space / 2), + Text( + '${listFileUploaded.length} ${AppLocalizations.of(context).attachments}', + style: AttachmentHeaderComposerWidgetStyle.labelTextSize + ), + const SizedBox(width: AttachmentHeaderComposerWidgetStyle.space), + Container( + decoration: const BoxDecoration( + color: AttachmentHeaderComposerWidgetStyle.sizeLabelBackground, + borderRadius: BorderRadius.all(Radius.circular(AttachmentHeaderComposerWidgetStyle.sizeLabelRadius)) + ), + padding: AttachmentHeaderComposerWidgetStyle.sizeLabelPadding, + child: Text( + filesize(listFileUploaded.totalSize, 0), + style: AttachmentHeaderComposerWidgetStyle.sizeLabelTextSize, + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart b/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart new file mode 100644 index 0000000000..42084ef581 --- /dev/null +++ b/lib/features/composer/presentation/widgets/attachment_item_composer_widget.dart @@ -0,0 +1,101 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:extended_text/extended_text.dart'; +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/attachment_item_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_progress_loading_composer_widget.dart'; +import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; + +typedef OnDeleteAttachmentAction = void Function(UploadFileState fileState); + +class AttachmentItemComposerWidget extends StatelessWidget with AppLoaderMixin { + + final _imagePaths = Get.find(); + + final UploadFileState fileState; + final double? maxWidth; + final EdgeInsetsGeometry? itemMargin; + final OnDeleteAttachmentAction? onDeleteAttachmentAction; + final Widget? buttonAction; + + AttachmentItemComposerWidget({ + super.key, + required this.fileState, + this.maxWidth, + this.itemMargin, + this.buttonAction, + this.onDeleteAttachmentAction, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(AttachmentItemComposerWidgetStyle.radius)), + border: Border.all(color: AttachmentItemComposerWidgetStyle.borderColor), + color: AttachmentItemComposerWidgetStyle.backgroundColor + ), + width: AttachmentItemComposerWidgetStyle.width, + padding: AttachmentItemComposerWidgetStyle.padding, + margin: itemMargin, + child: Row( + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + SvgPicture.asset( + fileState.getIcon(_imagePaths), + width: AttachmentItemComposerWidgetStyle.iconSize, + height: AttachmentItemComposerWidgetStyle.iconSize, + fit: BoxFit.fill + ), + const SizedBox(width: AttachmentItemComposerWidgetStyle.space), + Expanded( + child: ExtendedText( + fileState.fileName, + maxLines: 1, + overflowWidget: const TextOverflowWidget( + position: TextOverflowPosition.middle, + child: Text( + '...', + style: AttachmentItemComposerWidgetStyle.dotsLabelTextStyle, + ), + ), + style: AttachmentItemComposerWidgetStyle.labelTextStyle, + ), + ), + const SizedBox(width: AttachmentItemComposerWidgetStyle.space), + Text( + filesize(fileState.fileSize), + style: AttachmentItemComposerWidgetStyle.sizeLabelTextStyle + ), + ], + ), + AttachmentProgressLoadingComposerWidget( + fileState: fileState, + padding: AttachmentItemComposerWidgetStyle.progressLoadingPadding, + ) + ], + ), + ), + const SizedBox(width: AttachmentItemComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icCancel, + iconSize: AttachmentItemComposerWidgetStyle.deleteIconSize, + borderRadius: AttachmentItemComposerWidgetStyle.deleteIconRadius, + padding: AttachmentItemComposerWidgetStyle.deleteIconPadding, + iconColor: AttachmentItemComposerWidgetStyle.deleteIconColor, + onTapActionCallback: () => onDeleteAttachmentAction?.call(fileState), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/attachment_progress_loading_composer_widget.dart b/lib/features/composer/presentation/widgets/attachment_progress_loading_composer_widget.dart new file mode 100644 index 0000000000..fd2ea63961 --- /dev/null +++ b/lib/features/composer/presentation/widgets/attachment_progress_loading_composer_widget.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:percent_indicator/linear_percent_indicator.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/attachment_progress_loading_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; +import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_status.dart'; + +class AttachmentProgressLoadingComposerWidget extends StatelessWidget { + + final UploadFileState fileState; + final EdgeInsetsGeometry? padding; + + const AttachmentProgressLoadingComposerWidget({ + super.key, + required this.fileState, + this.padding, + }); + + @override + Widget build(BuildContext context) { + switch (fileState.uploadStatus) { + case UploadFileStatus.waiting: + return Padding( + padding: padding ?? EdgeInsets.zero, + child: const LinearProgressIndicator( + color: AttachmentProgressLoadingComposerWidgetStyle.progressColor, + minHeight: AttachmentProgressLoadingComposerWidgetStyle.height, + backgroundColor: AttachmentProgressLoadingComposerWidgetStyle.backgroundColor, + ), + ); + case UploadFileStatus.uploading: + return Padding( + padding: padding ?? EdgeInsets.zero, + child: LinearPercentIndicator( + padding: EdgeInsets.zero, + lineHeight:AttachmentProgressLoadingComposerWidgetStyle.height, + percent: fileState.percentUploading > 1.0 + ? 1.0 + : fileState.percentUploading, + barRadius: const Radius.circular(AttachmentProgressLoadingComposerWidgetStyle.radius), + backgroundColor: AttachmentProgressLoadingComposerWidgetStyle.backgroundColor, + progressColor: AttachmentProgressLoadingComposerWidgetStyle.progressColor, + ), + ); + case UploadFileStatus.uploadFailed: + case UploadFileStatus.succeed: + return const SizedBox.shrink(); + } + } +} diff --git a/lib/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart b/lib/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart new file mode 100644 index 0000000000..158edfe242 --- /dev/null +++ b/lib/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart @@ -0,0 +1,62 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/extensions/string_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/avatar/gradient_circle_avatar_icon.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/draggable_recipient_tag_widget_style.dart'; + +class DraggableRecipientTagWidget extends StatelessWidget { + + final EmailAddress emailAddress; + + final _imagePaths = Get.find(); + + DraggableRecipientTagWidget({ + super.key, + required this.emailAddress + }); + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.grab, + child: Container( + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(DraggableRecipientTagWidgetStyle.radius)), + ), + color: DraggableRecipientTagWidgetStyle.backgroundColor + ), + padding: DraggableRecipientTagWidgetStyle.padding, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (emailAddress.displayName.isNotEmpty) + GradientCircleAvatarIcon( + colors: emailAddress.avatarColors, + label: emailAddress.displayName.firstLetterToUpperCase, + labelFontSize: DraggableRecipientTagWidgetStyle.avatarLabelFontSize, + iconSize: DraggableRecipientTagWidgetStyle.avatarIconSize, + ), + Padding( + padding: DraggableRecipientTagWidgetStyle.labelPadding, + child: DefaultTextStyle( + style: DraggableRecipientTagWidgetStyle.labelTextStyle, + child: Text(emailAddress.asString()), + ), + ), + SvgPicture.asset( + _imagePaths.icClose, + colorFilter: DraggableRecipientTagWidgetStyle.deleteIconColor.asFilter(), + fit: BoxFit.fill + ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/drop_down_menu_header_style_widget.dart b/lib/features/composer/presentation/widgets/drop_down_menu_header_style_widget.dart index 06052a4af9..ae79b42c0c 100644 --- a/lib/features/composer/presentation/widgets/drop_down_menu_header_style_widget.dart +++ b/lib/features/composer/presentation/widgets/drop_down_menu_header_style_widget.dart @@ -47,19 +47,25 @@ class DropDownMenuHeaderStyleWidget extends StatelessWidget { customButton: icon, onChanged: onChanged, onMenuStateChange: onMenuStateChange, - itemHeight: heightItem, - buttonHeight: heightItem, - itemPadding: const EdgeInsets.symmetric(horizontal: 12), - dropdownWidth: dropdownWidth, - dropdownMaxHeight: 200, - dropdownDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: Colors.white, + buttonStyleData: ButtonStyleData(height: heightItem), + dropdownStyleData: DropdownStyleData( + maxHeight: 200, + width: dropdownWidth, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: Colors.white), + elevation: 4, + offset: const Offset(0.0, -8.0), + scrollbarTheme: ScrollbarThemeData( + radius: const Radius.circular(40), + thickness: MaterialStateProperty.all(6), + thumbVisibility: MaterialStateProperty.all(true), + ) ), - offset: const Offset(0.0, -8.0), - dropdownElevation: 4, - scrollbarRadius: const Radius.circular(40), - scrollbarThickness: 6, + menuItemStyleData: MenuItemStyleData( + height: heightItem, + padding: const EdgeInsets.symmetric(horizontal: 12) + ) ), ), ); diff --git a/lib/features/composer/presentation/widgets/email_address_input_builder.dart b/lib/features/composer/presentation/widgets/email_address_input_builder.dart deleted file mode 100644 index 8a061bb652..0000000000 --- a/lib/features/composer/presentation/widgets/email_address_input_builder.dart +++ /dev/null @@ -1,494 +0,0 @@ - -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:core/core.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:model/model.dart'; -import 'package:super_tag_editor/tag_editor.dart'; -import 'package:super_tag_editor/widgets/rich_text_widget.dart'; -import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/suggestion_email_address.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -typedef OnSuggestionEmailAddress = Future> Function(String word); -typedef OnUpdateListEmailAddressAction = void Function(PrefixEmailAddress, List); -typedef OnAddEmailAddressTypeAction = void Function(PrefixEmailAddress); -typedef OnDeleteEmailAddressTypeAction = void Function(PrefixEmailAddress); -typedef OnShowFullListEmailAddressAction = void Function(PrefixEmailAddress); -typedef OnFocusEmailAddressChangeAction = void Function(PrefixEmailAddress, bool); - -class EmailAddressInputBuilder { - - static const _suggestionBoxRadius = 20.0; - final BuildContext _context; - final ImagePaths _imagePaths; - final ExpandMode expandMode; - final PrefixEmailAddress _prefixEmailAddress; - final List _listEmailAddressType; - final TextEditingController? controller; - final bool? isInitial; - final FocusNode? focusNode; - final bool autoDisposeFocusNode; - final GlobalKey? keyTagEditor; - - List listEmailAddress = []; - - OnUpdateListEmailAddressAction? _onUpdateListEmailAddressAction; - OnSuggestionEmailAddress? _onSuggestionEmailAddress; - OnAddEmailAddressTypeAction? _onAddEmailAddressTypeAction; - OnDeleteEmailAddressTypeAction? _onDeleteEmailAddressTypeAction; - OnShowFullListEmailAddressAction? _onShowFullListEmailAddressAction; - OnFocusEmailAddressChangeAction? _onFocusEmailAddressChangeAction; - - Timer? _gapBetweenTagChangedAndFindSuggestion; - bool lastTagFocused = false; - - void addOnUpdateListEmailAddressAction(OnUpdateListEmailAddressAction onUpdateListEmailAddressAction) { - _onUpdateListEmailAddressAction = onUpdateListEmailAddressAction; - } - - void addOnSuggestionEmailAddress(OnSuggestionEmailAddress onSuggestionEmailAddress) { - _onSuggestionEmailAddress = onSuggestionEmailAddress; - } - - void addOnAddEmailAddressTypeAction(OnAddEmailAddressTypeAction onAddEmailAddressTypeAction) { - _onAddEmailAddressTypeAction = onAddEmailAddressTypeAction; - } - - void addOnDeleteEmailAddressTypeAction(OnDeleteEmailAddressTypeAction onDeleteEmailAddressTypeAction) { - _onDeleteEmailAddressTypeAction = onDeleteEmailAddressTypeAction; - } - - void addOnShowFullListEmailAddressAction(OnShowFullListEmailAddressAction onShowFullListEmailAddressAction) { - _onShowFullListEmailAddressAction = onShowFullListEmailAddressAction; - } - - void addOnFocusEmailAddressChangeAction(OnFocusEmailAddressChangeAction onFocusEmailAddressChangeAction) { - _onFocusEmailAddressChangeAction = onFocusEmailAddressChangeAction; - } - - EmailAddressInputBuilder( - this._context, - this._imagePaths, - this._prefixEmailAddress, - this.listEmailAddress, - this._listEmailAddressType, - { - this.isInitial, - this.controller, - this.focusNode, - this.autoDisposeFocusNode = true, - this.expandMode = ExpandMode.EXPAND, - this.keyTagEditor, - } - ); - - Widget build() { - return Row( - children: [ - Text('${_prefixEmailAddress.asName(_context)}:', - style: const TextStyle(fontSize: 15, color: AppColor.colorHintEmailAddressInput)), - const SizedBox(width: 8), - Expanded(child: Padding( - padding: EdgeInsets.only(right: _listEmailAddressType.length == 2 ? 8 : 8), - child: _buildTagEditor())), - if (_prefixEmailAddress == PrefixEmailAddress.to) - Row(children: [ - if (!_listEmailAddressType.contains(PrefixEmailAddress.cc)) - buildTextIcon(AppLocalizations.of(_context).cc_email_address_prefix, - padding: const EdgeInsets.all(5), - textStyle: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - decoration: TextDecoration.underline, - color: AppColor.lineItemListColor), - onTap: () => _onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc)), - if (!_listEmailAddressType.contains(PrefixEmailAddress.bcc)) - buildTextIcon(AppLocalizations.of(_context).bcc_email_address_prefix, - padding: const EdgeInsets.all(5), - textStyle: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - decoration: TextDecoration.underline, - color: AppColor.lineItemListColor), - onTap: () => _onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc)), - const SizedBox(width: 10), - ]), - if (_prefixEmailAddress != PrefixEmailAddress.to) - buildIconWeb( - icon: SvgPicture.asset(_imagePaths.icCloseComposer, fit: BoxFit.fill), - onTap: () => _onDeleteEmailAddressTypeAction?.call(_prefixEmailAddress)) - ] - ); - } - - Widget _buildTagEditor() { - return StatefulBuilder(builder: (BuildContext context, StateSetter setState) { - final newListEmailAddress = _isCollapse ? listEmailAddress.sublist(0, 1) : listEmailAddress; - return FocusScope(child: Focus( - onFocusChange: (focus) => _onFocusEmailAddressChangeAction?.call(_prefixEmailAddress, focus), - child: TagEditor( - key: keyTagEditor, - length: newListEmailAddress.length, - controller: controller, - focusNode: focusNode, - autoDisposeFocusNode: autoDisposeFocusNode, - keyboardType: TextInputType.emailAddress, - textInputAction: TextInputAction.done, - debounceDuration: const Duration(milliseconds: 150), - hasAddButton: false, - tagSpacing: 8, - autofocus: _prefixEmailAddress != PrefixEmailAddress.to && listEmailAddress.isEmpty, - minTextFieldWidth: 20, - resetTextOnSubmitted: true, - suggestionsBoxElevation: _suggestionBoxRadius, - suggestionsBoxBackgroundColor: Colors.white, - suggestionsBoxRadius: 20, - suggestionsBoxMaxHeight: 350, - textStyle: const TextStyle(color: AppColor.colorEmailAddress, fontSize: 14, fontWeight: FontWeight.w500), - onFocusTagAction: (focused) { - setState(() { - lastTagFocused = focused; - }); - }, - onDeleteTagAction: () => _handleDeleteTagAction(setState, context), - onSelectOptionAction: (item) { - if (!_isDuplicatedRecipient(item.emailAddress.emailAddress)) { - setState(() => listEmailAddress.add(item.emailAddress)); - _onUpdateListEmailAddressAction?.call(_prefixEmailAddress, listEmailAddress); - } - }, - onSubmitted: (value) { - log('EmailAddressInputBuilder::_buildTagEditor(): onSubmitted: $value'); - if (!_isDuplicatedRecipient(value)) { - setState(() => listEmailAddress.add(EmailAddress(null, value))); - _onUpdateListEmailAddressAction?.call(_prefixEmailAddress, listEmailAddress); - } - }, - inputDecoration: const InputDecoration(border: InputBorder.none), - tagBuilder: (context, index) { - final isLastEmail = index == listEmailAddress.length - 1; - return Stack( - alignment: Alignment.centerRight, - children: [ - Padding( - padding: EdgeInsets.only( - top: kIsWeb ? 8 : 0, - right: _isCollapse ? 50 : 0), - child: InkWell( - onTap: () => _isCollapse - ? _onShowFullListEmailAddressAction?.call(_prefixEmailAddress) - : null, - child: Chip( - labelPadding: const EdgeInsets.only(left: 12, right: 12, bottom: 2), - label: Text(newListEmailAddress[index].asString(), maxLines: 1, overflow: kIsWeb ? null : TextOverflow.ellipsis), - deleteIcon: SvgPicture.asset(_imagePaths.icClose, fit: BoxFit.fill), - labelStyle: const TextStyle(color: Colors.black, fontSize: 17, fontWeight: FontWeight.normal), - backgroundColor: _getTagBackgroundColor(listEmailAddress[index], isLastEmail), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - side: _getTagBorderSide(listEmailAddress[index], isLastEmail), - ), - avatar: newListEmailAddress[index].displayName.isNotEmpty - ? CircleAvatar( - backgroundColor: AppColor.colorTextButton, - child: Text( - listEmailAddress[index].displayName[0].toUpperCase(), - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.w500 - ) - )) - : null, - onDeleted: () { - setState(() => listEmailAddress.removeAt(index)); - _onUpdateListEmailAddressAction?.call(_prefixEmailAddress, listEmailAddress); - }, - ) - ) - ), - if (_isCollapse) - _buildCounter( - context, - listEmailAddress.length - newListEmailAddress.length - ), - ] - ); - }, - onTagChanged: (String value) { - log('EmailAddressInputBuilder::_buildTagEditor(): onTagChanged: $value'); - if (!_isDuplicatedRecipient(value)) { - setState(() => listEmailAddress.add(EmailAddress(null, value))); - _onUpdateListEmailAddressAction?.call(_prefixEmailAddress, listEmailAddress); - } - _gapBetweenTagChangedAndFindSuggestion = Timer( - const Duration(seconds: 1), - _handleGapBetweenTagChangedAndFindSuggestion); - }, - findSuggestions: _findSuggestions, - useDefaultHighlight: false, - suggestionBuilder: ( - context, - tagEditorState, - suggestionEmailAddress, - index, - length, - highlight, - suggestionValid - ) { - switch (suggestionEmailAddress.state) { - case SuggestionEmailState.duplicated: - return _buildExistedSuggestionItem( - setState, - context, - suggestionEmailAddress.emailAddress, - suggestionValid - ); - default: - return _buildSuggestionItem( - setState, - context, - tagEditorState, - suggestionEmailAddress.emailAddress, - index, - length, - highlight, - suggestionValid - ); - } - }, - ) - )); - }); - } - - Widget _buildCounter(BuildContext context, int count) { - return Padding( - padding: const EdgeInsets.only(left: 8, top: kIsWeb ? 8 : 0), - child: InkWell( - onTap: () => _onShowFullListEmailAddressAction?.call(_prefixEmailAddress), - child: Chip( - labelPadding: const EdgeInsets.symmetric(horizontal: 8), - label: Text('+$count', maxLines: 1, overflow: kIsWeb ? null : TextOverflow.ellipsis), - labelStyle: const TextStyle(color: Colors.black, fontSize: 17, fontWeight: FontWeight.normal), - backgroundColor: AppColor.colorEmailAddressTag, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - side: const BorderSide(width: 0, color: AppColor.colorEmailAddressTag), - ), - ), - ), - ); - } - - bool get _isCollapse { - return listEmailAddress.length > 1 && expandMode == ExpandMode.COLLAPSE; - } - - Widget _buildExistedSuggestionItem( - StateSetter setState, - BuildContext context, - EmailAddress emailAddress, - String? suggestionValid - ) { - return Container( - padding: const EdgeInsets.all(8.0), - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(_suggestionBoxRadius))), - child: Container( - decoration: const BoxDecoration( - color: AppColor.colorBgMenuItemDropDownSelected, - borderRadius: BorderRadius.all(Radius.circular(_suggestionBoxRadius))), - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 8.0), - leading: _buildAvatarSuggestionItem(emailAddress), - title: _buildTitleSuggestionItem(emailAddress, suggestionValid), - subtitle: _buildSubtitleSuggestionItem(emailAddress, suggestionValid), - trailing: SvgPicture.asset(_imagePaths.icFilterSelected, - width: 24, - height: 24, - fit: BoxFit.fill), - ) - ) - ); - } - - Widget _buildSuggestionItem( - StateSetter setState, - BuildContext context, - TagsEditorState tagEditorState, - EmailAddress emailAddress, - int index, - int length, - bool highlight, - String? suggestionValid - ) { - return Container( - color: highlight ? AppColor.colorItemSelected : Colors.white, - child: ListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16), - leading: _buildAvatarSuggestionItem(emailAddress), - title: _buildTitleSuggestionItem(emailAddress, suggestionValid), - subtitle: _buildSubtitleSuggestionItem(emailAddress, suggestionValid), - onTap: () { - setState(() => listEmailAddress.add(emailAddress)); - _onUpdateListEmailAddressAction?.call(_prefixEmailAddress, listEmailAddress); - tagEditorState.resetTextField(); - tagEditorState.closeSuggestionBox(); - }, - ), - ); - } - - Widget _buildAvatarSuggestionItem(EmailAddress emailAddress) { - return Container( - width: 40, - height: 40, - alignment: Alignment.center, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AppColor.avatarColor, - border: Border.all( - color: AppColor.colorShadowBgContentEmail, - width: 1.0 - ) - ), - child: Text( - emailAddress.asString().isNotEmpty - ? emailAddress.asString()[0].toUpperCase() - : '', - style: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w600 - ) - ) - ); - } - - Widget _buildTitleSuggestionItem(EmailAddress emailAddress, String? suggestionValid) { - return RichTextWidget( - textOrigin: emailAddress.asString(), - wordSearched: suggestionValid ?? '' - ); - } - - Widget? _buildSubtitleSuggestionItem(EmailAddress emailAddress, String? suggestionValid) { - if (emailAddress.displayName.isNotEmpty && emailAddress.emailAddress.isNotEmpty) { - return RichTextWidget( - textOrigin: emailAddress.emailAddress, - wordSearched: suggestionValid ?? '', - styleTextOrigin: const TextStyle( - color: AppColor.colorHintSearchBar, - fontSize: 13, - fontWeight: FontWeight.normal - ), - styleWordSearched: const TextStyle( - color: Colors.black, - fontSize: 13, - fontWeight: FontWeight.bold - ) - ); - } else { - return null; - } - } - - FutureOr> _findSuggestions(String query) async { - log('EmailAddressInputBuilder::_findSuggestions():query: $query'); - if (_gapBetweenTagChangedAndFindSuggestion?.isActive ?? false) { - log('EmailAddressInputBuilder::_findSuggestions(): return empty'); - return []; - } - - final processedQuery = query.trim(); - if (processedQuery.isEmpty) { - return []; - } - - final tmailSuggestion = List.empty(growable: true); - if (processedQuery.isNotEmpty && _onSuggestionEmailAddress != null) { - tmailSuggestion.addAll( - (await _onSuggestionEmailAddress!(processedQuery)) - .map((emailAddress) => _toSuggestionEmailAddress(emailAddress, listEmailAddress))); - } - - tmailSuggestion.addAll(_matchedSuggestionEmailAddress(processedQuery, listEmailAddress)); - - final currentTextOnTextField = controller?.text ?? ''; - if (currentTextOnTextField.isEmpty) { - return []; - } - - return tmailSuggestion; - } - - bool _isEmailAddressValid(String emailAddress) => GetUtils.isEmail(emailAddress); - - bool _isDuplicatedRecipient(String inputEmail) { - if (inputEmail.isEmpty) { - return false; - } - return listEmailAddress - .map((emailAddress) => emailAddress.email) - .whereNotNull() - .contains(inputEmail); - } - - SuggestionEmailAddress _toSuggestionEmailAddress(EmailAddress item, List addedEmailAddresses) { - if (addedEmailAddresses.contains(item)) { - return SuggestionEmailAddress(item, state: SuggestionEmailState.duplicated); - } else { - return SuggestionEmailAddress(item); - } - } - - Iterable _matchedSuggestionEmailAddress(String query, List addedEmailAddress) { - return addedEmailAddress.where((addedMail) => addedMail.emailAddress.contains(query)) - .map((emailAddress) => SuggestionEmailAddress(emailAddress, state: SuggestionEmailState.duplicated)); - } - - void _handleGapBetweenTagChangedAndFindSuggestion() { - log('EmailAddressInputBuilder::_handleGapBetweenTagChangedAndFindSuggestion(): Timeout'); - } - - Color _getTagBackgroundColor(EmailAddress emailCurrent, bool isLastEmail) { - if (lastTagFocused && isLastEmail) { - return AppColor.colorItemRecipientSelected; - } else { - return _isEmailAddressValid(emailCurrent.emailAddress) - ? AppColor.colorEmailAddressTag - : Colors.white; - } - } - - BorderSide _getTagBorderSide(EmailAddress emailCurrent, bool isLastEmail) { - if (lastTagFocused && isLastEmail) { - return const BorderSide( - width: 1, - color: AppColor.primaryColor); - } else { - return BorderSide( - width: _isEmailAddressValid(emailCurrent.emailAddress) ? 0 : 1, - color: _isEmailAddressValid(emailCurrent.emailAddress) - ? AppColor.colorEmailAddressTag - : AppColor.colorBorderEmailAddressInvalid); - } - } - - void _handleDeleteTagAction(StateSetter setState, BuildContext context) { - log('EmailAddressInputBuilder::_handleDeleteTagAction()'); - if (listEmailAddress.isNotEmpty) { - setState(() { - listEmailAddress.removeLast(); - }); - _onUpdateListEmailAddressAction?.call(_prefixEmailAddress, listEmailAddress); - } - } -} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/insert_image_dialog_builder.dart b/lib/features/composer/presentation/widgets/insert_image_dialog_builder.dart deleted file mode 100644 index 17a5fd3bc5..0000000000 --- a/lib/features/composer/presentation/widgets/insert_image_dialog_builder.dart +++ /dev/null @@ -1,185 +0,0 @@ - -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/views/button/icon_button_web.dart'; -import 'package:core/utils/build_utils.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:model/upload/file_info.dart'; -import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/image_source.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/inline_image.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -import 'package:tmail_ui_user/main/routes/route_navigation.dart'; - -typedef InsertImageActionCallback = Function(InlineImage image); - -class InsertImageDialogBuilder { - - final _inputFileController = TextEditingController(); - final _inputUrlController = TextEditingController(); - - final InsertImageActionCallback? insertActionCallback; - final BuildContext _context; - - FileInfo? fileSelected; - String? validateFailed; - - InsertImageDialogBuilder(this._context, { - this.insertActionCallback - }); - - Future show() async { - await showDialog( - context: _context, - barrierColor: AppColor.colorDefaultCupertinoActionSheet, - builder: (BuildContext context) { - return PointerInterceptor( - child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) { - return AlertDialog( - title: Text(AppLocalizations.of(context).insertImage, - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - color: Colors.black)), - titleTextStyle: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 20, - color: Colors.black), - titlePadding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), - scrollable: true, - elevation: 10, - content: Container( - color: Colors.white, - width: 300, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(AppLocalizations.of(context).selectFromFile, - style: const TextStyle( - fontSize: 14, - color: Colors.black, - fontWeight: FontWeight.w500)), - const SizedBox(height: 10), - TextFormField( - controller: _inputFileController, - readOnly: true, - decoration: InputDecoration( - prefixIcon: buildButtonWrapText( - AppLocalizations.of(context).chooseImage, - radius: 5, - height: 30, - padding: const EdgeInsets.only(right: 8), - textStyle: const TextStyle( - color: Colors.black, - fontSize: 14, - fontWeight: FontWeight.normal), - bgColor: AppColor.colorShadowComposer, - onTap: () => _selectFromFile(setState)), - suffixIcon: fileSelected != null - ? IconButton( - splashRadius: 10, - icon: const Icon(Icons.close), - onPressed: () { - setState(() { - fileSelected = null; - _inputFileController.text = ''; - }); - }) - : const SizedBox.shrink(), - errorText: validateFailed, - errorMaxLines: 2, - border: InputBorder.none, - )), - const SizedBox(height: 20), - Text(AppLocalizations.of(context).urlLink, - style: const TextStyle( - fontSize: 14, - color: Colors.black, - fontWeight: FontWeight.w500)), - const SizedBox(height: 10), - TextField( - controller: _inputUrlController, - textInputAction: TextInputAction.done, - decoration: InputDecoration( - border: const OutlineInputBorder(), - hintText: AppLocalizations.of(context).urlLink, - errorText: validateFailed, - errorMaxLines: 2, - ), - ), - ]), - ), - actionsPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), - actions: [ - buildButtonWrapText( - AppLocalizations.of(context).cancel, - radius: 5, - textStyle: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.normal), - bgColor: AppColor.colorShadowComposer, - onTap: () => popBack()), - buildButtonWrapText( - AppLocalizations.of(context).insert, - radius: 5, - textStyle: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w500), - onTap: () => _insertImageAction(context, setState)) - ], - ); - }), - ); - }); - } - - void _selectFromFile(StateSetter setState) async { - final filePickerResult = await FilePicker.platform.pickFiles( - type: FileType.image, - withReadStream: true); - - final platformFile = filePickerResult?.files.single; - if (platformFile != null) { - fileSelected = FileInfo( - platformFile.name, - BuildUtils.isWeb ? '' : platformFile.path ?? '', - platformFile.size, - readStream: platformFile.readStream); - - setState(() { - _inputFileController.text = fileSelected!.fileName; - }); - } - } - - void _insertImageAction(BuildContext context, StateSetter setState) { - final inputFile = _inputFileController.text; - final inputUrl = _inputUrlController.text; - - if (inputFile.isEmpty && inputUrl.isEmpty) { - setState(() { - validateFailed = AppLocalizations.of(context).insertImageErrorFileEmpty; - }); - } else if (inputFile.isNotEmpty && inputUrl.isNotEmpty) { - setState(() { - validateFailed = AppLocalizations.of(context).insertImageErrorDuplicate; - }); - } else if (inputFile.isNotEmpty && fileSelected != null) { - if (insertActionCallback != null) { - insertActionCallback!.call(InlineImage(ImageSource.local, fileInfo: fileSelected)); - } - popBack(); - } else { - if (insertActionCallback != null) { - insertActionCallback!.call(InlineImage(ImageSource.network, link: inputUrl)); - } - popBack(); - } - } -} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart b/lib/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart new file mode 100644 index 0000000000..43debb9c3f --- /dev/null +++ b/lib/features/composer/presentation/widgets/insert_image_loading_bar_widget.dart @@ -0,0 +1,55 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/widget/circle_loading_widget.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/download_image_as_base64_state.dart'; +import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; + +class InsertImageLoadingBarWidget extends StatelessWidget { + + final Either viewState; + final Either uploadInlineViewState; + final EdgeInsetsGeometry? padding; + + const InsertImageLoadingBarWidget({ + super.key, + required this.viewState, + required this.uploadInlineViewState, + this.padding, + }); + + @override + Widget build(BuildContext context) { + return uploadInlineViewState.fold( + (failure) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is DownloadingImageAsBase64) { + return CircleLoadingWidget(padding: padding); + } else { + return const SizedBox.shrink(); + } + } + ); + }, + (success) { + if (success is UploadingAttachmentUploadState) { + return CircleLoadingWidget(padding: padding); + } else { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is DownloadingImageAsBase64) { + return CircleLoadingWidget(padding: padding); + } else { + return const SizedBox.shrink(); + } + } + ); + } + } + ); + } +} diff --git a/lib/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart new file mode 100644 index 0000000000..2d174c4a05 --- /dev/null +++ b/lib/features/composer/presentation/widgets/mobile/app_bar_composer_widget.dart @@ -0,0 +1,69 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/mobile_app_bar_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class AppBarComposerWidget extends StatelessWidget { + + final bool isSendButtonEnabled; + final VoidCallback onCloseViewAction; + final VoidCallback sendMessageAction; + final OnOpenContextMenuAction openContextMenuAction; + + final _imagePaths = Get.find(); + + AppBarComposerWidget({ + super.key, + required this.isSendButtonEnabled, + required this.onCloseViewAction, + required this.sendMessageAction, + required this.openContextMenuAction, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: MobileAppBarComposerWidgetStyle.height, + color: MobileAppBarComposerWidgetStyle.backgroundColor, + padding: MobileAppBarComposerWidgetStyle.padding, + child: Row( + children: [ + TMailButtonWidget.fromIcon( + icon: _imagePaths.icCancel, + backgroundColor: Colors.transparent, + tooltipMessage: AppLocalizations.of(context).saveAndClose, + iconSize: MobileAppBarComposerWidgetStyle.iconSize, + iconColor: MobileAppBarComposerWidgetStyle.iconColor, + padding: MobileAppBarComposerWidgetStyle.iconPadding, + onTapActionCallback: onCloseViewAction + ), + const Spacer(), + TMailButtonWidget.fromIcon( + icon: isSendButtonEnabled + ? _imagePaths.icSendMobile + : _imagePaths.icSendDisable, + backgroundColor: Colors.transparent, + padding: MobileAppBarComposerWidgetStyle.iconPadding, + iconSize: MobileAppBarComposerWidgetStyle.sendButtonIconSize, + tooltipMessage: AppLocalizations.of(context).send, + onTapActionCallback: sendMessageAction, + ), + const SizedBox(width: MobileAppBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icMore, + iconColor: MobileAppBarComposerWidgetStyle.iconColor, + borderRadius: MobileAppBarComposerWidgetStyle.iconRadius, + backgroundColor: Colors.transparent, + padding: MobileAppBarComposerWidgetStyle.iconPadding, + iconSize: MobileAppBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).more, + onTapActionAtPositionCallback: openContextMenuAction, + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart new file mode 100644 index 0000000000..67e266f390 --- /dev/null +++ b/lib/features/composer/presentation/widgets/mobile/landscape_app_bar_composer_widget.dart @@ -0,0 +1,73 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/mobile_app_bar_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class LandscapeAppBarComposerWidget extends StatelessWidget { + + final bool isSendButtonEnabled; + final VoidCallback onCloseViewAction; + final VoidCallback sendMessageAction; + final OnOpenContextMenuAction openContextMenuAction; + + final _imagePaths = Get.find(); + + LandscapeAppBarComposerWidget({ + super.key, + required this.isSendButtonEnabled, + required this.onCloseViewAction, + required this.sendMessageAction, + required this.openContextMenuAction, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: MobileAppBarComposerWidgetStyle.height, + color: MobileAppBarComposerWidgetStyle.backgroundColor, + padding: MobileAppBarComposerWidgetStyle.padding, + child: SafeArea( + top: false, + bottom: false, + child: Row( + children: [ + TMailButtonWidget.fromIcon( + icon: _imagePaths.icCancel, + backgroundColor: Colors.transparent, + tooltipMessage: AppLocalizations.of(context).saveAndClose, + iconSize: MobileAppBarComposerWidgetStyle.iconSize, + iconColor: MobileAppBarComposerWidgetStyle.iconColor, + padding: MobileAppBarComposerWidgetStyle.iconPadding, + onTapActionCallback: onCloseViewAction + ), + const Spacer(), + TMailButtonWidget.fromIcon( + icon: isSendButtonEnabled + ? _imagePaths.icSendMobile + : _imagePaths.icSendDisable, + backgroundColor: Colors.transparent, + padding: MobileAppBarComposerWidgetStyle.iconPadding, + iconSize: MobileAppBarComposerWidgetStyle.sendButtonIconSize, + tooltipMessage: AppLocalizations.of(context).send, + onTapActionCallback: sendMessageAction, + ), + const SizedBox(width: MobileAppBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icMore, + iconColor: MobileAppBarComposerWidgetStyle.iconColor, + borderRadius: MobileAppBarComposerWidgetStyle.iconRadius, + backgroundColor: Colors.transparent, + padding: MobileAppBarComposerWidgetStyle.iconPadding, + iconSize: MobileAppBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).more, + onTapActionAtPositionCallback: openContextMenuAction, + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart b/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart new file mode 100644 index 0000000000..e219c6f3f5 --- /dev/null +++ b/lib/features/composer/presentation/widgets/mobile/mobile_attachment_composer_widget.dart @@ -0,0 +1,75 @@ +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/list/sliver_grid_delegate_fixed_height.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/attachment_item_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/mobile/mobile_attachment_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_item_composer_widget.dart'; +import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; + +class MobileAttachmentComposerWidget extends StatelessWidget { + + final List listFileUploaded; + final OnDeleteAttachmentAction onDeleteAttachmentAction; + + final _responsiveUtils = Get.find(); + + MobileAttachmentComposerWidget({ + super.key, + required this.listFileUploaded, + required this.onDeleteAttachmentAction, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: MobileAttachmentComposerWidgetStyle.padding, + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_responsiveUtils.isLandscapeMobile(context)) + SizedBox( + width: _responsiveUtils.getSizeScreenWidth(context) * 0.7, + child: GridView.builder( + reverse: true, + primary: false, + shrinkWrap: true, + itemCount: listFileUploaded.length, + gridDelegate: const SliverGridDelegateFixedHeight( + height: MobileAttachmentComposerWidgetStyle.listItemHeight, + crossAxisCount: MobileAttachmentComposerWidgetStyle.maxItemRow, + crossAxisSpacing: MobileAttachmentComposerWidgetStyle.listItemSpace, + ), + itemBuilder: (context, index) { + return AttachmentItemComposerWidget( + fileState: listFileUploaded[index], + itemMargin: MobileAttachmentComposerWidgetStyle.itemMargin, + onDeleteAttachmentAction: onDeleteAttachmentAction + ); + } + ), + ) + else + SizedBox( + width: AttachmentItemComposerWidgetStyle.width, + child: ListView.builder( + reverse: true, + shrinkWrap: true, + primary: false, + itemCount: listFileUploaded.length, + itemBuilder: (context, index) { + return AttachmentItemComposerWidget( + fileState: listFileUploaded[index], + itemMargin: MobileAttachmentComposerWidgetStyle.itemMargin, + onDeleteAttachmentAction: onDeleteAttachmentAction + ); + } + ), + ) + ] + ), + ); + } +} diff --git a/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart b/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart new file mode 100644 index 0000000000..363b8a0967 --- /dev/null +++ b/lib/features/composer/presentation/widgets/mobile/mobile_editor_widget.dart @@ -0,0 +1,36 @@ + +import 'package:core/presentation/utils/html_transformer/html_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:rich_text_composer/rich_text_composer.dart'; + +typedef OnCreatedEditorAction = Function(BuildContext context, HtmlEditorApi editorApi, String content); +typedef OnLoadCompletedEditorAction = Function(HtmlEditorApi editorApi, WebUri? url); + +class MobileEditorWidget extends StatelessWidget { + + final String content; + final TextDirection direction; + final OnCreatedEditorAction onCreatedEditorAction; + final OnLoadCompletedEditorAction onLoadCompletedEditorAction; + + const MobileEditorWidget({ + super.key, + required this.content, + required this.direction, + required this.onCreatedEditorAction, + required this.onLoadCompletedEditorAction, + }); + + @override + Widget build(BuildContext context) { + return HtmlEditor( + key: const Key('mobile_editor'), + minHeight: 550, + addDefaultSelectionMenuItems: false, + initialContent: content, + customStyleCss: HtmlUtils.customCssStyleHtmlEditor(direction: direction), + onCreated: (editorApi) => onCreatedEditorAction.call(context, editorApi, content), + onCompleted: onLoadCompletedEditorAction, + ); + } +} diff --git a/lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart new file mode 100644 index 0000000000..a1d76f14d8 --- /dev/null +++ b/lib/features/composer/presentation/widgets/mobile/tablet_bottom_bar_composer_widget.dart @@ -0,0 +1,77 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/mobile/tablet_bottom_bar_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class TabletBottomBarComposerWidget extends StatelessWidget { + + final VoidCallback deleteComposerAction; + final VoidCallback saveToDraftAction; + final VoidCallback sendMessageAction; + final OnRequestReadReceiptAction? requestReadReceiptAction; + + final _imagePaths = Get.find(); + + TabletBottomBarComposerWidget({ + super.key, + required this.deleteComposerAction, + required this.saveToDraftAction, + required this.sendMessageAction, + this.requestReadReceiptAction, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: TabletBottomBarComposerWidgetStyle.padding, + color: TabletBottomBarComposerWidgetStyle.backgroundColor, + child: Row( + children: [ + const Spacer(), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icDeleteMailbox, + borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius, + padding: TabletBottomBarComposerWidgetStyle.iconPadding, + iconSize: TabletBottomBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).delete, + onTapActionCallback: deleteComposerAction, + ), + const SizedBox(width: TabletBottomBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icReadReceipt, + borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius, + padding: TabletBottomBarComposerWidgetStyle.iconPadding, + iconSize: TabletBottomBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).requestReadReceipt, + onTapActionAtPositionCallback: requestReadReceiptAction, + ), + const SizedBox(width: TabletBottomBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icSaveToDraft, + borderRadius: TabletBottomBarComposerWidgetStyle.iconRadius, + padding: TabletBottomBarComposerWidgetStyle.iconPadding, + iconSize: TabletBottomBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).saveAsDraft, + onTapActionCallback: saveToDraftAction, + ), + const SizedBox(width: TabletBottomBarComposerWidgetStyle.sendButtonSpace), + TMailButtonWidget( + text: AppLocalizations.of(context).send, + icon: _imagePaths.icSend, + iconAlignment: TextDirection.rtl, + padding: TabletBottomBarComposerWidgetStyle.sendButtonPadding, + iconSize: TabletBottomBarComposerWidgetStyle.iconSize, + iconSpace: TabletBottomBarComposerWidgetStyle.sendButtonIconSpace, + textStyle: TabletBottomBarComposerWidgetStyle.sendButtonTextStyle, + backgroundColor: TabletBottomBarComposerWidgetStyle.sendButtonBackgroundColor, + borderRadius: TabletBottomBarComposerWidgetStyle.sendButtonRadius, + onTapActionCallback: sendMessageAction, + ) + ] + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/recipient_composer_widget.dart b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart new file mode 100644 index 0000000000..958ea64f4f --- /dev/null +++ b/lib/features/composer/presentation/widgets/recipient_composer_widget.dart @@ -0,0 +1,491 @@ + +import 'dart:async'; +import 'dart:math'; + +import 'package:collection/collection.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/email/prefix_email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:model/mailbox/expand_mode.dart'; +import 'package:super_tag_editor/tag_editor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/prefix_recipient_state.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/suggestion_email_address.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/recipient_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_tag_item_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_constants.dart'; + +typedef OnSuggestionEmailAddress = Future> Function(String word); +typedef OnUpdateListEmailAddressAction = void Function(PrefixEmailAddress prefix, List newData); +typedef OnAddEmailAddressTypeAction = void Function(PrefixEmailAddress prefix); +typedef OnDeleteEmailAddressTypeAction = void Function(PrefixEmailAddress prefix); +typedef OnShowFullListEmailAddressAction = void Function(PrefixEmailAddress prefix); +typedef OnFocusEmailAddressChangeAction = void Function(PrefixEmailAddress prefix, bool isFocus); +typedef OnRemoveDraggableEmailAddressAction = void Function(DraggableEmailAddress draggableEmailAddress); +typedef OnDeleteTagAction = void Function(EmailAddress emailAddress); + +class RecipientComposerWidget extends StatefulWidget { + + final PrefixEmailAddress prefix; + final List listEmailAddress; + final ExpandMode expandMode; + final PrefixRecipientState ccState; + final PrefixRecipientState bccState; + final bool? isInitial; + final FocusNode? focusNode; + final bool autoDisposeFocusNode; + final GlobalKey? keyTagEditor; + final FocusNode? nextFocusNode; + final TextEditingController? controller; + final OnUpdateListEmailAddressAction? onUpdateListEmailAddressAction; + final OnSuggestionEmailAddress? onSuggestionEmailAddress; + final OnAddEmailAddressTypeAction? onAddEmailAddressTypeAction; + final OnDeleteEmailAddressTypeAction? onDeleteEmailAddressTypeAction; + final OnShowFullListEmailAddressAction? onShowFullListEmailAddressAction; + final OnFocusEmailAddressChangeAction? onFocusEmailAddressChangeAction; + final OnRemoveDraggableEmailAddressAction? onRemoveDraggableEmailAddressAction; + final VoidCallback? onFocusNextAddressAction; + final EdgeInsetsGeometry? padding; + final EdgeInsetsGeometry? margin; + + const RecipientComposerWidget({ + super.key, + required this.prefix, + required this.listEmailAddress, + this.ccState = PrefixRecipientState.disabled, + this.bccState = PrefixRecipientState.disabled, + this.isInitial, + this.controller, + this.focusNode, + this.autoDisposeFocusNode = true, + this.expandMode = ExpandMode.EXPAND, + this.keyTagEditor, + this.nextFocusNode, + this.padding, + this.margin, + this.onUpdateListEmailAddressAction, + this.onSuggestionEmailAddress, + this.onAddEmailAddressTypeAction, + this.onDeleteEmailAddressTypeAction, + this.onShowFullListEmailAddressAction, + this.onFocusEmailAddressChangeAction, + this.onFocusNextAddressAction, + this.onRemoveDraggableEmailAddressAction, + }); + + @override + State createState() => _RecipientComposerWidgetState(); +} + +class _RecipientComposerWidgetState extends State { + + Timer? _gapBetweenTagChangedAndFindSuggestion; + bool _lastTagFocused = false; + bool _isDragging = false; + late List _currentListEmailAddress; + + final _imagePaths = Get.find(); + + @override + void initState() { + super.initState(); + _currentListEmailAddress = widget.listEmailAddress; + } + + @override + void didUpdateWidget(covariant RecipientComposerWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.listEmailAddress != widget.listEmailAddress) { + _currentListEmailAddress = widget.listEmailAddress; + } + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: RecipientComposerWidgetStyle.borderColor, + width: 1 + ) + ) + ), + padding: widget.padding, + margin: widget.margin, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: RecipientComposerWidgetStyle.labelMargin, + child: Text( + '${widget.prefix.asName(context)}:', + style: RecipientComposerWidgetStyle.labelTextStyle + ), + ), + const SizedBox(width: RecipientComposerWidgetStyle.space), + Expanded( + child: FocusScope( + child: Focus( + onFocusChange: (focus) => widget.onFocusEmailAddressChangeAction?.call(widget.prefix, focus), + onKey: (focusNode, event) { + if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { + widget.nextFocusNode?.requestFocus(); + widget.onFocusNextAddressAction?.call(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: StatefulBuilder( + builder: (context, stateSetter) { + if (PlatformInfo.isWeb) { + return DragTarget( + builder: (context, candidateData, rejectedData) { + return TagEditor( + key: widget.keyTagEditor, + length: _collapsedListEmailAddress.length, + controller: widget.controller, + focusNode: widget.focusNode, + enableBorder: _isDragging, + borderRadius: RecipientComposerWidgetStyle.enableBorderRadius, + enableBorderColor: RecipientComposerWidgetStyle.enableBorderColor, + autoDisposeFocusNode: widget.autoDisposeFocusNode, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, + hasAddButton: false, + tagSpacing: RecipientComposerWidgetStyle.tagSpacing, + autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, + minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, + resetTextOnSubmitted: true, + suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, + suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, + suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, + suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, + suggestionBoxWidth: _getSuggestionBoxWidth(constraints.maxWidth), + textStyle: RecipientComposerWidgetStyle.inputTextStyle, + onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), + onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), + onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), + onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), + inputDecoration: const InputDecoration(border: InputBorder.none), + tagBuilder: (context, index) { + final currentEmailAddress = _currentListEmailAddress[index]; + final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; + + return RecipientTagItemWidget( + prefix: widget.prefix, + currentEmailAddress: currentEmailAddress, + currentListEmailAddress: _currentListEmailAddress, + collapsedListEmailAddress: _collapsedListEmailAddress, + isLatestEmail: isLatestEmail, + isCollapsed: _isCollapse, + isLatestTagFocused: _lastTagFocused, + onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), + onShowFullAction: widget.onShowFullListEmailAddressAction, + ); + }, + onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), + findSuggestions: _findSuggestions, + useDefaultHighlight: false, + suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { + return RecipientSuggestionItemWidget( + suggestionState: suggestionEmailAddress.state, + emailAddress: suggestionEmailAddress.emailAddress, + suggestionValid: suggestionValid, + highlight: highlight, + onSelectedAction: (emailAddress) { + stateSetter(() => _currentListEmailAddress.add(emailAddress)); + _updateListEmailAddressAction(); + tagEditorState.resetTextField(); + tagEditorState.closeSuggestionBox(); + }, + ); + }, + ); + }, + onAccept: (draggableEmailAddress) => _handleAcceptDraggableEmailAddressAction(draggableEmailAddress, stateSetter), + onLeave: (draggableEmailAddress) { + if (_isDragging) { + stateSetter(() => _isDragging = false); + } + }, + onMove: (details) { + if (!_isDragging) { + stateSetter(() => _isDragging = true); + } + }, + ); + } else { + return TagEditor( + key: widget.keyTagEditor, + length: _collapsedListEmailAddress.length, + controller: widget.controller, + focusNode: widget.focusNode, + autoDisposeFocusNode: widget.autoDisposeFocusNode, + keyboardType: TextInputType.emailAddress, + textInputAction: TextInputAction.done, + debounceDuration: RecipientComposerWidgetStyle.suggestionDebounceDuration, + hasAddButton: false, + tagSpacing: RecipientComposerWidgetStyle.tagSpacing, + autofocus: widget.prefix != PrefixEmailAddress.to && _currentListEmailAddress.isEmpty, + minTextFieldWidth: RecipientComposerWidgetStyle.minTextFieldWidth, + resetTextOnSubmitted: true, + suggestionsBoxElevation: RecipientComposerWidgetStyle.suggestionsBoxElevation, + suggestionsBoxBackgroundColor: RecipientComposerWidgetStyle.suggestionsBoxBackgroundColor, + suggestionsBoxRadius: RecipientComposerWidgetStyle.suggestionsBoxRadius, + suggestionsBoxMaxHeight: RecipientComposerWidgetStyle.suggestionsBoxMaxHeight, + suggestionBoxWidth: _getSuggestionBoxWidth(constraints.maxWidth), + textStyle: RecipientComposerWidgetStyle.inputTextStyle, + onFocusTagAction: (focused) => _handleFocusTagAction.call(focused, stateSetter), + onDeleteTagAction: () => _handleDeleteLatestTagAction.call(stateSetter), + onSelectOptionAction: (item) => _handleSelectOptionAction.call(item, stateSetter), + onSubmitted: (value) => _handleSubmitTagAction.call(value, stateSetter), + inputDecoration: const InputDecoration(border: InputBorder.none), + tagBuilder: (context, index) { + final currentEmailAddress = _currentListEmailAddress[index]; + final isLatestEmail = currentEmailAddress == _currentListEmailAddress.last; + + return RecipientTagItemWidget( + prefix: widget.prefix, + currentEmailAddress: currentEmailAddress, + currentListEmailAddress: _currentListEmailAddress, + collapsedListEmailAddress: _collapsedListEmailAddress, + isLatestEmail: isLatestEmail, + isCollapsed: _isCollapse, + isLatestTagFocused: _lastTagFocused, + onDeleteTagAction: (emailAddress) => _handleDeleteTagAction.call(emailAddress, stateSetter), + onShowFullAction: widget.onShowFullListEmailAddressAction, + ); + }, + onTagChanged: (value) => _handleOnTagChangeAction.call(value, stateSetter), + findSuggestions: _findSuggestions, + useDefaultHighlight: false, + suggestionBuilder: (context, tagEditorState, suggestionEmailAddress, index, length, highlight, suggestionValid) { + return RecipientSuggestionItemWidget( + suggestionState: suggestionEmailAddress.state, + emailAddress: suggestionEmailAddress.emailAddress, + suggestionValid: suggestionValid, + highlight: highlight, + onSelectedAction: (emailAddress) { + stateSetter(() => _currentListEmailAddress.add(emailAddress)); + _updateListEmailAddressAction(); + tagEditorState.resetTextField(); + tagEditorState.closeSuggestionBox(); + }, + ); + }, + ); + } + }, + ) + ) + ) + ), + const SizedBox(width: RecipientComposerWidgetStyle.space), + if (widget.prefix == PrefixEmailAddress.to && widget.ccState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).cc_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.cc), + ), + if (widget.prefix == PrefixEmailAddress.to && widget.bccState == PrefixRecipientState.disabled) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).bcc_email_address_prefix, + textStyle: RecipientComposerWidgetStyle.prefixButtonTextStyle, + backgroundColor: Colors.transparent, + padding: RecipientComposerWidgetStyle.prefixButtonPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onAddEmailAddressTypeAction?.call(PrefixEmailAddress.bcc), + ), + if (widget.prefix != PrefixEmailAddress.to) + TMailButtonWidget.fromIcon( + icon: _imagePaths.icClose, + backgroundColor: Colors.transparent, + iconColor: RecipientComposerWidgetStyle.deleteRecipientFieldIconColor, + iconSize: RecipientComposerWidgetStyle.deleteRecipientFieldIconSize, + padding: RecipientComposerWidgetStyle.deleteRecipientFieldIconPadding, + margin: RecipientComposerWidgetStyle.recipientMargin, + onTapActionCallback: () => widget.onDeleteEmailAddressTypeAction?.call(widget.prefix), + ) + ] + ), + ); + }); + } + + bool get _isCollapse => _currentListEmailAddress.length > 1 && widget.expandMode == ExpandMode.COLLAPSE; + + List get _collapsedListEmailAddress => _isCollapse + ? _currentListEmailAddress.sublist(0, 1) + : _currentListEmailAddress; + + FutureOr> _findSuggestions(String query) async { + if (_gapBetweenTagChangedAndFindSuggestion?.isActive ?? false) { + return []; + } + + final processedQuery = query.trim(); + if (processedQuery.isEmpty) { + return []; + } + + final tmailSuggestion = List.empty(growable: true); + if (processedQuery.length >= AppConstants.limitCharToStartSearch && + widget.onSuggestionEmailAddress != null) { + final listEmailAddress = await widget.onSuggestionEmailAddress!(processedQuery); + final listSuggestionEmailAddress = listEmailAddress.map((emailAddress) => _toSuggestionEmailAddress(emailAddress, _currentListEmailAddress)); + tmailSuggestion.addAll(listSuggestionEmailAddress); + } + + tmailSuggestion.addAll(_matchedSuggestionEmailAddress(processedQuery, _currentListEmailAddress)); + + final currentTextOnTextField = widget.controller?.text ?? ''; + if (currentTextOnTextField.isEmpty) { + return []; + } + + return tmailSuggestion.toSet().toList(); + } + + bool _isDuplicatedRecipient(String inputEmail) { + if (inputEmail.isEmpty) { + return false; + } + return _currentListEmailAddress + .map((emailAddress) => emailAddress.email) + .whereNotNull() + .contains(inputEmail); + } + + SuggestionEmailAddress _toSuggestionEmailAddress(EmailAddress item, List addedEmailAddresses) { + if (addedEmailAddresses.contains(item)) { + return SuggestionEmailAddress(item, state: SuggestionEmailState.duplicated); + } else { + return SuggestionEmailAddress(item); + } + } + + Iterable _matchedSuggestionEmailAddress(String query, List addedEmailAddress) { + return addedEmailAddress + .where((addedMail) => addedMail.emailAddress.contains(query)) + .map((emailAddress) => SuggestionEmailAddress( + emailAddress, + state: SuggestionEmailState.duplicated + )); + } + + void _handleGapBetweenTagChangedAndFindSuggestion() { + log('_RecipientComposerWidgetState::_handleGapBetweenTagChangedAndFindSuggestion:Timeout'); + } + + void _updateListEmailAddressAction() { + widget.onUpdateListEmailAddressAction?.call( + widget.prefix, + _currentListEmailAddress + ); + } + + void _handleFocusTagAction(bool focused, StateSetter stateSetter) { + stateSetter(() => _lastTagFocused = focused); + } + + void _handleDeleteLatestTagAction(StateSetter stateSetter) { + if (_currentListEmailAddress.isNotEmpty) { + stateSetter(_currentListEmailAddress.removeLast); + _updateListEmailAddressAction(); + } + } + + void _handleDeleteTagAction(EmailAddress emailAddress, StateSetter stateSetter) { + if (_currentListEmailAddress.isNotEmpty) { + stateSetter(() => _currentListEmailAddress.remove(emailAddress)); + _updateListEmailAddressAction(); + } + } + + void _handleSelectOptionAction( + SuggestionEmailAddress suggestionEmailAddress, + StateSetter stateSetter + ) { + if (!_isDuplicatedRecipient(suggestionEmailAddress.emailAddress.emailAddress)) { + stateSetter(() => _currentListEmailAddress.add(suggestionEmailAddress.emailAddress)); + _updateListEmailAddressAction(); + } + } + + void _handleSubmitTagAction( + String value, + StateSetter stateSetter + ) { + final textTrim = value.trim(); + if (!_isDuplicatedRecipient(textTrim)) { + stateSetter(() => _currentListEmailAddress.add(EmailAddress(null, textTrim))); + _updateListEmailAddressAction(); + } + } + + void _handleOnTagChangeAction( + String value, + StateSetter stateSetter + ) { + final textTrim = value.trim(); + if (!_isDuplicatedRecipient(textTrim)) { + stateSetter(() => _currentListEmailAddress.add(EmailAddress(null, textTrim))); + _updateListEmailAddressAction(); + } + _gapBetweenTagChangedAndFindSuggestion = Timer( + const Duration(seconds: 1), + _handleGapBetweenTagChangedAndFindSuggestion + ); + } + + void _handleAcceptDraggableEmailAddressAction( + DraggableEmailAddress draggableEmailAddress, + StateSetter stateSetter + ) { + log('_RecipientComposerWidgetState::_handleAcceptDraggableEmailAddressAction: $draggableEmailAddress'); + if (draggableEmailAddress.prefix != widget.prefix) { + if (!_currentListEmailAddress.contains(draggableEmailAddress.emailAddress)) { + stateSetter(() { + _currentListEmailAddress.add(draggableEmailAddress.emailAddress); + _isDragging = false; + }); + _updateListEmailAddressAction(); + } else { + if (_isDragging) { + stateSetter(() => _isDragging = false); + } + } + widget.onRemoveDraggableEmailAddressAction?.call(draggableEmailAddress); + } else { + if (_isDragging) { + stateSetter(() => _isDragging = false); + } + } + } + + double? _getSuggestionBoxWidth(double maxWidth) { + if (maxWidth < ResponsiveUtils.minTabletWidth) { + final newWidth = min(maxWidth, RecipientComposerWidgetStyle.suggestionBoxWidth); + return newWidth; + } else { + return null; + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart new file mode 100644 index 0000000000..db96fa245b --- /dev/null +++ b/lib/features/composer/presentation/widgets/recipient_suggestion_item_widget.dart @@ -0,0 +1,96 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:super_tag_editor/widgets/rich_text_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/suggestion_email_address.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/recipient_suggestion_item_widget_style.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_suggestion_item_widget.dart'; + +typedef OnSelectedRecipientSuggestionAction = Function(EmailAddress emailAddress); + +class RecipientSuggestionItemWidget extends StatelessWidget { + + final SuggestionEmailState suggestionState; + final EmailAddress emailAddress; + final String? suggestionValid; + final bool highlight; + final OnSelectedRecipientSuggestionAction? onSelectedAction; + + final _imagePaths = Get.find(); + + RecipientSuggestionItemWidget({ + super.key, + required this.suggestionState, + required this.emailAddress, + this.suggestionValid, + this.highlight = false, + this.onSelectedAction, + }); + + @override + Widget build(BuildContext context) { + if (suggestionState == SuggestionEmailState.duplicated) { + return Container( + margin: RecipientSuggestionItemWidgetStyle.suggestionDuplicatedMargin, + decoration: const BoxDecoration( + color: AppColor.colorBgMenuItemDropDownSelected, + borderRadius: BorderRadius.all(Radius.circular(RecipientSuggestionItemWidgetStyle.radius)) + ), + child: Material( + type: MaterialType.transparency, + child: ListTile( + contentPadding: RecipientSuggestionItemWidgetStyle.labelDuplicatedPadding, + leading: AvatarSuggestionItemWidget(emailAddress: emailAddress), + title: RichTextWidget( + textOrigin: emailAddress.asString(), + wordSearched: suggestionValid ?? '' + ), + subtitle: emailAddress.emailAddress.isNotEmpty + ? RichTextWidget( + textOrigin: emailAddress.emailAddress, + wordSearched: suggestionValid ?? '', + styleTextOrigin: RecipientSuggestionItemWidgetStyle.labelTextStyle, + styleWordSearched: RecipientSuggestionItemWidgetStyle.labelHighlightTextStyle + ) + : null, + trailing: SvgPicture.asset( + _imagePaths.icFilterSelected, + width: RecipientSuggestionItemWidgetStyle.selectedIconSize, + height: RecipientSuggestionItemWidgetStyle.selectedIconSize, + fit: BoxFit.fill + ), + ), + ) + ); + } else { + return Container( + color: highlight ? AppColor.colorItemSelected : Colors.white, + child: Material( + type: MaterialType.transparency, + child: ListTile( + contentPadding: RecipientSuggestionItemWidgetStyle.labelPadding, + leading: AvatarSuggestionItemWidget(emailAddress: emailAddress), + title: RichTextWidget( + textOrigin: emailAddress.asString(), + wordSearched: suggestionValid ?? '' + ), + subtitle: emailAddress.emailAddress.isNotEmpty + ? RichTextWidget( + textOrigin: emailAddress.emailAddress, + wordSearched: suggestionValid ?? '', + styleTextOrigin: RecipientSuggestionItemWidgetStyle.labelTextStyle, + styleWordSearched: RecipientSuggestionItemWidgetStyle.labelHighlightTextStyle + ) + : null, + onTap: () => onSelectedAction?.call(emailAddress), + ), + ), + ); + } + } +} diff --git a/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart new file mode 100644 index 0000000000..5734c96a1e --- /dev/null +++ b/lib/features/composer/presentation/widgets/recipient_tag_item_widget.dart @@ -0,0 +1,174 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/extensions/string_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/avatar/gradient_circle_avatar_icon.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/email/prefix_email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/draggable_email_address.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/recipient_tag_item_widget_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/draggable_recipient_tag_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/recipient_composer_widget.dart'; + +class RecipientTagItemWidget extends StatelessWidget { + + final bool isCollapsed; + final bool isLatestTagFocused; + final bool isLatestEmail; + final PrefixEmailAddress prefix; + final EmailAddress currentEmailAddress; + final List currentListEmailAddress; + final List collapsedListEmailAddress; + final OnShowFullListEmailAddressAction? onShowFullAction; + final OnDeleteTagAction? onDeleteTagAction; + + final _imagePaths = Get.find(); + + RecipientTagItemWidget({ + super.key, + required this.prefix, + required this.currentEmailAddress, + required this.currentListEmailAddress, + required this.collapsedListEmailAddress, + this.isCollapsed = false, + this.isLatestTagFocused = false, + this.isLatestEmail = false, + this.onShowFullAction, + this.onDeleteTagAction, + }); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: AlignmentDirectional.centerEnd, + children: [ + if (PlatformInfo.isWeb) + Padding( + padding: EdgeInsetsDirectional.only( + top: 8, + end: isCollapsed ? 40 : 0), + child: InkWell( + onTap: () => isCollapsed + ? onShowFullAction?.call(prefix) + : null, + child: Draggable( + data: DraggableEmailAddress(emailAddress: currentEmailAddress, prefix: prefix), + feedback: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), + childWhenDragging: DraggableRecipientTagWidget(emailAddress: currentEmailAddress), + child: MouseRegion( + cursor: SystemMouseCursors.grab, + child: Chip( + labelPadding: EdgeInsetsDirectional.symmetric( + horizontal: 4, + vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 + ), + label: Text( + currentEmailAddress.asString(), + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + ), + deleteIcon: SvgPicture.asset(_imagePaths.icClose, fit: BoxFit.fill), + labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, + backgroundColor: _getTagBackgroundColor(), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(RecipientTagItemWidgetStyle.radius)), + side: _getTagBorderSide(), + ), + avatar: currentEmailAddress.displayName.isNotEmpty + ? GradientCircleAvatarIcon( + colors: currentEmailAddress.avatarColors, + label: currentEmailAddress.displayName.firstLetterToUpperCase, + labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, + iconSize: RecipientTagItemWidgetStyle.avatarIconSize, + ) + : null, + onDeleted: () => onDeleteTagAction?.call(currentEmailAddress), + ), + ), + ) + ), + ) + else + Padding( + padding: EdgeInsetsDirectional.only(end: isCollapsed ? 40 : 0), + child: InkWell( + onTap: () => isCollapsed + ? onShowFullAction?.call(prefix) + : null, + child: Chip( + labelPadding: EdgeInsetsDirectional.symmetric( + horizontal: 4, + vertical: DirectionUtils.isDirectionRTLByHasAnyRtl(currentEmailAddress.asString()) ? 0 : 2 + ), + label: Text( + currentEmailAddress.asString(), + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + ), + deleteIcon: SvgPicture.asset(_imagePaths.icClose, fit: BoxFit.fill), + labelStyle: RecipientTagItemWidgetStyle.labelTextStyle, + backgroundColor: _getTagBackgroundColor(), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(RecipientTagItemWidgetStyle.radius)), + side: _getTagBorderSide(), + ), + avatar: currentEmailAddress.displayName.isNotEmpty + ? GradientCircleAvatarIcon( + colors: currentEmailAddress.avatarColors, + label: currentEmailAddress.displayName.firstLetterToUpperCase, + labelFontSize: RecipientTagItemWidgetStyle.avatarLabelFontSize, + iconSize: RecipientTagItemWidgetStyle.avatarIconSize, + ) + : null, + onDeleted: () => onDeleteTagAction?.call(currentEmailAddress), + ) + ), + ), + if (isCollapsed) + TMailButtonWidget.fromText( + margin: RecipientTagItemWidgetStyle.counterMargin, + text: '+${currentListEmailAddress.length - collapsedListEmailAddress.length}', + onTapActionCallback: () => onShowFullAction?.call(prefix), + borderRadius: RecipientTagItemWidgetStyle.radius, + textStyle: RecipientTagItemWidgetStyle.labelTextStyle, + padding: PlatformInfo.isWeb + ? RecipientTagItemWidgetStyle.counterPadding + : RecipientTagItemWidgetStyle.mobileCounterPadding, + backgroundColor: AppColor.colorEmailAddressTag, + ) + ] + ); + } + + Color _getTagBackgroundColor() { + if (isLatestTagFocused && isLatestEmail) { + return AppColor.colorItemRecipientSelected; + } else if (GetUtils.isEmail(currentEmailAddress.emailAddress)) { + return AppColor.colorEmailAddressTag; + } else { + return Colors.white; + } + } + + BorderSide _getTagBorderSide() { + if (isLatestTagFocused && isLatestEmail) { + return const BorderSide(width: 1, color: AppColor.primaryColor); + } else if (GetUtils.isEmail(currentEmailAddress.emailAddress)) { + return BorderSide.none; + } else { + return const BorderSide( + width: 1, + color: AppColor.colorBorderEmailAddressInvalid + ); + } + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/subject_composer_widget.dart b/lib/features/composer/presentation/widgets/subject_composer_widget.dart new file mode 100644 index 0000000000..cfc4d8a7a4 --- /dev/null +++ b/lib/features/composer/presentation/widgets/subject_composer_widget.dart @@ -0,0 +1,59 @@ +import 'package:core/presentation/views/text/text_field_builder.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/subject_composer_widget_style.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class SubjectComposerWidget extends StatelessWidget { + + final FocusNode? focusNode; + final TextEditingController textController; + final ValueChanged? onTextChange; + final EdgeInsetsGeometry? margin; + final EdgeInsetsGeometry? padding; + + const SubjectComposerWidget({ + super.key, + required this.focusNode, + required this.textController, + required this.onTextChange, + this.margin, + this.padding, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide( + color: SubjectComposerWidgetStyle.borderColor, + width: 1 + ) + ), + ), + margin: margin, + padding: padding, + child: Row( + children: [ + Text( + '${AppLocalizations.of(context).subject_email}:', + style: SubjectComposerWidgetStyle.labelTextStyle + ), + const SizedBox(width:SubjectComposerWidgetStyle.space), + Expanded( + child: TextFieldBuilder( + cursorColor: SubjectComposerWidgetStyle.cursorColor, + focusNode: focusNode, + onTextChange: onTextChange, + maxLines: 1, + textDirection: DirectionUtils.getDirectionByLanguage(context), + textStyle: SubjectComposerWidgetStyle.inputTextStyle, + controller: textController, + ) + ) + ] + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/title_composer_widget.dart b/lib/features/composer/presentation/widgets/title_composer_widget.dart new file mode 100644 index 0000000000..bcf8d049f7 --- /dev/null +++ b/lib/features/composer/presentation/widgets/title_composer_widget.dart @@ -0,0 +1,29 @@ +import 'package:core/presentation/extensions/capitalize_extension.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/title_composer_widget_style.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class TitleComposerWidget extends StatelessWidget { + + final String emailSubject; + + const TitleComposerWidget({ + super.key, + required this.emailSubject, + }); + + @override + Widget build(BuildContext context) { + return Text( + emailSubject.isNotEmpty == true + ? emailSubject + : AppLocalizations.of(context).new_message.capitalizeFirstEach, + maxLines: 1, + textAlign: TextAlign.center, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: TitleComposerWidgetStyle.textStyle, + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/toolbar_rich_text_builder.dart b/lib/features/composer/presentation/widgets/toolbar_rich_text_builder.dart deleted file mode 100644 index e6c0928e74..0000000000 --- a/lib/features/composer/presentation/widgets/toolbar_rich_text_builder.dart +++ /dev/null @@ -1,220 +0,0 @@ -import 'dart:developer'; - -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/resources/image_paths.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/base/widget/drop_down_button_widget.dart'; -import 'package:tmail_ui_user/features/base/widget/popup_menu_overlay_widget.dart'; -import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; -import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/dropdown_menu_font_status.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/font_name_type.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/order_list_type.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/paragraph_type.dart'; -import 'package:tmail_ui_user/features/composer/presentation/model/rich_text_style_type.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/drop_down_menu_header_style_widget.dart'; - -class ToolbarRichTextWebBuilder extends StatelessWidget with RichTextButtonMixin { - - final RichTextWebController richTextWebController; - final ImagePaths _imagePaths = Get.find(); - final EdgeInsetsGeometry? padding; - - ToolbarRichTextWebBuilder({ - Key? key, - required this.richTextWebController, - this.padding, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Obx(() { - final codeViewEnabled = richTextWebController.codeViewEnabled; - final opacity = codeViewEnabled ? 0.5 : 1.0; - - return Container( - padding: padding, - alignment: Alignment.centerLeft, - child: Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - runSpacing: 8, - children: [ - AbsorbPointer( - absorbing: codeViewEnabled, - child: DropDownMenuHeaderStyleWidget( - icon: buildWrapIconStyleText( - isSelected: richTextWebController.isMenuHeaderStyleOpen, - icon: SvgPicture.asset(RichTextStyleType.headerStyle.getIcon(_imagePaths), - color: AppColor.colorDefaultRichTextButton.withOpacity(opacity), - fit: BoxFit.fill), - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), - tooltip: RichTextStyleType.headerStyle.getTooltipButton(context) - ), - items: HeaderStyleType.values, - onMenuStateChange: (isOpen) { - log('ComposerView::_buildToolbarRichTextWidget(): MenuHeaderStyleStatus: $isOpen'); - final newStatus = isOpen - ? DropdownMenuFontStatus.open - : DropdownMenuFontStatus.closed; - richTextWebController.menuHeaderStyleStatus.value = newStatus; - }, - onChanged: (newStyle) => richTextWebController.applyHeaderStyle(newStyle)), - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: Container( - width: 130, - padding: const EdgeInsets.only(left: 4.0, right: 4.0), - child: DropDownButtonWidget( - items: FontNameType.values, - itemSelected: richTextWebController.selectedFontName.value, - onChanged: (newFont) => richTextWebController.applyNewFontStyle(newFont), - onMenuStateChange: (isOpen) { - log('ComposerView::_buildToolbarRichTextWidget(): MenuFontStatus: $isOpen'); - final newStatus = isOpen - ? DropdownMenuFontStatus.open - : DropdownMenuFontStatus.closed; - richTextWebController.menuFontStatus.value = newStatus; - }, - heightItem: 40, - sizeIconChecked: 16, - radiusButton: 8, - opacity: opacity, - dropdownWidth: 200, - colorButton: richTextWebController.isMenuFontOpen - ? AppColor.colorBackgroundWrapIconStyleCode - : Colors.white, - iconArrowDown: SvgPicture.asset(_imagePaths.icStyleArrowDown), - tooltip: RichTextStyleType.fontName.getTooltipButton(context), - supportSelectionIcon: true)), - ), - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: AbsorbPointer( - absorbing: codeViewEnabled, - child: buildWrapIconStyleText( - icon: buildIconWithTooltip( - path: RichTextStyleType.textColor.getIcon(_imagePaths), - color: richTextWebController.selectedTextColor.value, - tooltip: RichTextStyleType.textColor.getTooltipButton(context), - opacity: opacity), - onTap: () => richTextWebController.applyRichTextStyle(context, RichTextStyleType.textColor)), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: AbsorbPointer( - absorbing: codeViewEnabled, - child: buildWrapIconStyleText( - padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 7), - spacing: 3, - icon: buildIconColorBackgroundText( - iconData: RichTextStyleType.textBackgroundColor.getIconData(), - colorSelected: richTextWebController.selectedTextBackgroundColor.value, - tooltip: RichTextStyleType.textBackgroundColor.getTooltipButton(context), - opacity: opacity), - onTap: () => richTextWebController.applyRichTextStyle(context, RichTextStyleType.textBackgroundColor)), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: buildWrapIconStyleText( - hasDropdown: false, - padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 5), - icon: Wrap(children: [ - AbsorbPointer( - absorbing: codeViewEnabled, - child: buildIconStyleText( - path: RichTextStyleType.bold.getIcon(_imagePaths), - isSelected: richTextWebController.isTextStyleTypeSelected(RichTextStyleType.bold), - tooltip: RichTextStyleType.bold.getTooltipButton(context), - opacity: opacity, - onTap: () => richTextWebController.applyRichTextStyle(context, RichTextStyleType.bold)), - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: buildIconStyleText( - path: RichTextStyleType.italic.getIcon(_imagePaths), - isSelected: richTextWebController.isTextStyleTypeSelected(RichTextStyleType.italic), - tooltip: RichTextStyleType.italic.getTooltipButton(context), - opacity: opacity, - onTap: () => richTextWebController.applyRichTextStyle(context, RichTextStyleType.italic)), - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: buildIconStyleText( - path: RichTextStyleType.underline.getIcon(_imagePaths), - isSelected: richTextWebController.isTextStyleTypeSelected(RichTextStyleType.underline), - tooltip: RichTextStyleType.underline.getTooltipButton(context), - opacity: opacity, - onTap: () => richTextWebController.applyRichTextStyle(context, RichTextStyleType.underline)), - ), - AbsorbPointer( - absorbing: codeViewEnabled, - child: buildIconStyleText( - path: RichTextStyleType.strikeThrough.getIcon(_imagePaths), - isSelected: richTextWebController.isTextStyleTypeSelected( - RichTextStyleType.strikeThrough), - tooltip: RichTextStyleType.strikeThrough.getTooltipButton(context), - opacity: opacity, - onTap: () => richTextWebController.applyRichTextStyle(context, RichTextStyleType.strikeThrough)), - ) - ])), - ), - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: AbsorbPointer( - absorbing: codeViewEnabled, - child: PopupMenuOverlayWidget( - controller: richTextWebController.menuParagraphController, - listButtonAction: ParagraphType.values - .map((paragraph) => paragraph.buildButtonWidget( - context, - _imagePaths, - (paragraph) => richTextWebController.applyParagraphType(paragraph))) - .toList(), - iconButton: buildWrapIconStyleText( - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), - spacing: 3, - isSelected: richTextWebController.focusMenuParagraph.value, - icon: buildIconWithTooltip( - path: richTextWebController.selectedParagraph.value.getIcon(_imagePaths), - color: AppColor.colorDefaultRichTextButton, - opacity: opacity, - tooltip: RichTextStyleType.paragraph.getTooltipButton(context))), - ), - ), - ), - Padding( - padding: const EdgeInsets.only(right: 4.0), - child: AbsorbPointer( - absorbing: codeViewEnabled, - child: PopupMenuOverlayWidget( - controller: richTextWebController.menuOrderListController, - listButtonAction: OrderListType.values - .map((orderType) => orderType.buildButtonWidget( - context, - _imagePaths, - (orderType) => richTextWebController.applyOrderListType(orderType))) - .toList(), - iconButton: buildWrapIconStyleText( - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), - spacing: 3, - isSelected: richTextWebController.focusMenuOrderList.value, - icon: buildIconWithTooltip( - path: richTextWebController.selectedOrderList.value.getIcon(_imagePaths), - color: AppColor.colorDefaultRichTextButton, - opacity: opacity, - tooltip: RichTextStyleType.orderList.getTooltipButton(context))), - ), - ), - ) - ] - ), - ); - }); - } -} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/web/attachment_composer_widget.dart b/lib/features/composer/presentation/widgets/web/attachment_composer_widget.dart new file mode 100644 index 0000000000..9f18d17f1f --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/attachment_composer_widget.dart @@ -0,0 +1,80 @@ + +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/attachment_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_item_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/attachment_header_composer_widget.dart'; +import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_state.dart'; + +typedef OnToggleExpandAttachmentAction = void Function(bool isCollapsed); + +class AttachmentComposerWidget extends StatefulWidget { + + final List listFileUploaded; + final bool isCollapsed; + final OnDeleteAttachmentAction onDeleteAttachmentAction; + final OnToggleExpandAttachmentAction onToggleExpandAttachmentAction; + + const AttachmentComposerWidget({ + super.key, + required this.listFileUploaded, + required this.isCollapsed, + required this.onDeleteAttachmentAction, + required this.onToggleExpandAttachmentAction, + }); + + @override + State createState() => _AttachmentComposerWidgetState(); +} + +class _AttachmentComposerWidgetState extends State { + + bool _isCollapsed = false; + + @override + void initState() { + super.initState(); + _isCollapsed = widget.isCollapsed; + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + color: AttachmentComposerWidgetStyle.backgroundColor, + boxShadow: AttachmentComposerWidgetStyle.shadow + ), + width: double.infinity, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AttachmentHeaderComposerWidget( + listFileUploaded: widget.listFileUploaded, + isCollapsed: _isCollapsed, + onToggleExpandAction: (isCollapsed) { + setState(() => _isCollapsed = !isCollapsed); + widget.onToggleExpandAttachmentAction(!isCollapsed); + }, + ), + if (!_isCollapsed) + Container( + width: double.infinity, + padding: AttachmentComposerWidgetStyle.listItemPadding, + constraints: const BoxConstraints(maxHeight: AttachmentComposerWidgetStyle.maxHeight), + child: SingleChildScrollView( + child: Wrap( + spacing: AttachmentComposerWidgetStyle.listItemSpace, + runSpacing: AttachmentComposerWidgetStyle.listItemSpace, + children: widget.listFileUploaded + .map((file) => AttachmentItemComposerWidget( + fileState: file, + onDeleteAttachmentAction: widget.onDeleteAttachmentAction + )) + .toList(), + ), + ), + ), + ] + ), + ); + } +} diff --git a/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart new file mode 100644 index 0000000000..d2855acefc --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/bottom_bar_composer_widget.dart @@ -0,0 +1,144 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/bottom_bar_composer_widget_style.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnRequestReadReceiptAction = Function(RelativeRect position); + +class BottomBarComposerWidget extends StatelessWidget { + + final bool isCodeViewEnabled; + final bool isFormattingOptionsEnabled; + final VoidCallback openRichToolbarAction; + final VoidCallback attachFileAction; + final VoidCallback insertImageAction; + final VoidCallback showCodeViewAction; + final VoidCallback deleteComposerAction; + final VoidCallback saveToDraftAction; + final VoidCallback sendMessageAction; + final OnRequestReadReceiptAction? requestReadReceiptAction; + + final _imagePaths = Get.find(); + + BottomBarComposerWidget({ + super.key, + required this.isCodeViewEnabled, + required this.isFormattingOptionsEnabled, + required this.openRichToolbarAction, + required this.attachFileAction, + required this.insertImageAction, + required this.showCodeViewAction, + required this.deleteComposerAction, + required this.saveToDraftAction, + required this.sendMessageAction, + this.requestReadReceiptAction, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: BottomBarComposerWidgetStyle.padding, + color: BottomBarComposerWidgetStyle.backgroundColor, + child: Row( + children: [ + TMailButtonWidget.fromIcon( + icon: _imagePaths.icRichToolbar, + borderRadius: BottomBarComposerWidgetStyle.iconRadius, + padding: BottomBarComposerWidgetStyle.richTextIconPadding, + backgroundColor: isFormattingOptionsEnabled + ? BottomBarComposerWidgetStyle.selectedBackgroundColor + : Colors.transparent, + iconSize: BottomBarComposerWidgetStyle.richTextIconSize, + iconColor: isFormattingOptionsEnabled + ? BottomBarComposerWidgetStyle.selectedIconColor + : BottomBarComposerWidgetStyle.iconColor, + tooltipMessage: AppLocalizations.of(context).formattingOptions, + onTapActionCallback: openRichToolbarAction, + ), + const SizedBox(width: BottomBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icAttachFile, + iconColor: BottomBarComposerWidgetStyle.iconColor, + borderRadius: BottomBarComposerWidgetStyle.iconRadius, + backgroundColor: Colors.transparent, + padding: BottomBarComposerWidgetStyle.iconPadding, + iconSize: BottomBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).attach_file, + onTapActionCallback: attachFileAction, + ), + const SizedBox(width: BottomBarComposerWidgetStyle.space), + AbsorbPointer( + absorbing: isCodeViewEnabled, + child: TMailButtonWidget.fromIcon( + icon: _imagePaths.icInsertImage, + iconColor: BottomBarComposerWidgetStyle.iconColor, + borderRadius: BottomBarComposerWidgetStyle.iconRadius, + backgroundColor: Colors.transparent, + padding: BottomBarComposerWidgetStyle.iconPadding, + iconSize: BottomBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).insertImage, + onTapActionCallback: insertImageAction, + ), + ), + const SizedBox(width: BottomBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icStyleCodeView, + iconColor: isCodeViewEnabled + ? BottomBarComposerWidgetStyle.selectedIconColor + : BottomBarComposerWidgetStyle.iconColor, + borderRadius: BottomBarComposerWidgetStyle.iconRadius, + backgroundColor: isCodeViewEnabled + ? BottomBarComposerWidgetStyle.selectedBackgroundColor + : Colors.transparent, + padding: BottomBarComposerWidgetStyle.iconPadding, + iconSize: BottomBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).embedCode, + onTapActionCallback: showCodeViewAction, + ), + const Spacer(), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icDeleteMailbox, + borderRadius: BottomBarComposerWidgetStyle.iconRadius, + padding: BottomBarComposerWidgetStyle.iconPadding, + iconSize: BottomBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).delete, + onTapActionCallback: deleteComposerAction, + ), + const SizedBox(width: BottomBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icReadReceipt, + borderRadius: BottomBarComposerWidgetStyle.iconRadius, + padding: BottomBarComposerWidgetStyle.iconPadding, + iconSize: BottomBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).requestReadReceipt, + onTapActionAtPositionCallback: requestReadReceiptAction, + ), + const SizedBox(width: BottomBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icSaveToDraft, + borderRadius: BottomBarComposerWidgetStyle.iconRadius, + padding: BottomBarComposerWidgetStyle.iconPadding, + iconSize: BottomBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).saveAsDraft, + onTapActionCallback: saveToDraftAction, + ), + const SizedBox(width: BottomBarComposerWidgetStyle.sendButtonSpace), + TMailButtonWidget( + text: AppLocalizations.of(context).send, + icon: _imagePaths.icSend, + iconAlignment: TextDirection.rtl, + padding: BottomBarComposerWidgetStyle.sendButtonPadding, + iconSize: BottomBarComposerWidgetStyle.iconSize, + iconSpace: BottomBarComposerWidgetStyle.sendButtonIconSpace, + textStyle: BottomBarComposerWidgetStyle.sendButtonTextStyle, + backgroundColor: BottomBarComposerWidgetStyle.sendButtonBackgroundColor, + borderRadius: BottomBarComposerWidgetStyle.sendButtonRadius, + onTapActionCallback: sendMessageAction, + ) + ] + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/web/desktop_app_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/desktop_app_bar_composer_widget.dart new file mode 100644 index 0000000000..ae7cc28f39 --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/desktop_app_bar_composer_widget.dart @@ -0,0 +1,107 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/app_bar_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/minimize_composer_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/title_composer_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class DesktopAppBarComposerWidget extends StatelessWidget { + + final String emailSubject; + final VoidCallback onCloseViewAction; + final ScreenDisplayMode? displayMode; + final OnChangeDisplayModeAction? onChangeDisplayModeAction; + final BoxConstraints? constraints; + + final _imagePaths = Get.find(); + + DesktopAppBarComposerWidget({ + super.key, + required this.emailSubject, + required this.onCloseViewAction, + this.displayMode, + this.onChangeDisplayModeAction, + this.constraints, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: AppBarComposerWidgetStyle.height, + padding: AppBarComposerWidgetStyle.padding, + color: AppBarComposerWidgetStyle.backgroundColor, + child: Stack( + children: [ + Center( + child: Container( + constraints: constraints != null + ? BoxConstraints(maxWidth: constraints!.maxWidth / 2) + : null, + child: TitleComposerWidget(emailSubject: emailSubject), + ), + ), + if (onChangeDisplayModeAction != null) + Align( + alignment: AlignmentDirectional.centerEnd, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + TMailButtonWidget.fromIcon( + icon: _imagePaths.icMinimize, + backgroundColor: Colors.transparent, + tooltipMessage: AppLocalizations.of(context).minimize, + iconSize: AppBarComposerWidgetStyle.iconSize, + iconColor: AppBarComposerWidgetStyle.iconColor, + padding: AppBarComposerWidgetStyle.iconPadding, + onTapActionCallback: () => onChangeDisplayModeAction!(ScreenDisplayMode.minimize) + ), + const SizedBox(width: AppBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: displayMode == ScreenDisplayMode.fullScreen + ? _imagePaths.icFullScreenExit + : _imagePaths.icFullScreen, + backgroundColor: Colors.transparent, + tooltipMessage: AppLocalizations.of(context).fullscreen, + iconSize: AppBarComposerWidgetStyle.iconSize, + iconColor: AppBarComposerWidgetStyle.iconColor, + padding: AppBarComposerWidgetStyle.iconPadding, + onTapActionCallback: () => onChangeDisplayModeAction!( + displayMode == ScreenDisplayMode.fullScreen + ? ScreenDisplayMode.normal + : ScreenDisplayMode.fullScreen + ) + ), + const SizedBox(width: AppBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icCancel, + backgroundColor: Colors.transparent, + tooltipMessage: AppLocalizations.of(context).saveAndClose, + iconSize: AppBarComposerWidgetStyle.iconSize, + iconColor: AppBarComposerWidgetStyle.iconColor, + padding: AppBarComposerWidgetStyle.iconPadding, + onTapActionCallback: onCloseViewAction + ), + ] + ), + ) + else + Align( + alignment: AlignmentDirectional.centerEnd, + child: TMailButtonWidget.fromIcon( + icon: _imagePaths.icCancel, + backgroundColor: Colors.transparent, + tooltipMessage: AppLocalizations.of(context).saveAndClose, + iconSize: AppBarComposerWidgetStyle.iconSize, + iconColor: AppBarComposerWidgetStyle.iconColor, + padding: AppBarComposerWidgetStyle.iconPadding, + onTapActionCallback: onCloseViewAction + ), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/web/drop_zone_widget.dart b/lib/features/composer/presentation/widgets/web/drop_zone_widget.dart new file mode 100644 index 0000000000..6391feb457 --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/drop_zone_widget.dart @@ -0,0 +1,92 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:dotted_border/dotted_border.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/web/drop_zone_widget_style.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnAddAttachmentFromDropZone = Function(Attachment attachment); + +class DropZoneWidget extends StatefulWidget { + + final double? width; + final double? height; + final OnAddAttachmentFromDropZone? addAttachmentFromDropZone; + + const DropZoneWidget({ + super.key, + this.width, + this.height, + this.addAttachmentFromDropZone + }); + + @override + State createState() => _DropZoneWidgetState(); +} + +class _DropZoneWidgetState extends State { + + final _imagePaths = Get.find(); + + bool _isDragging = false; + + @override + Widget build(BuildContext context) { + return DragTarget( + builder: (context, candidateData, rejectedData) { + if (_isDragging) { + return Padding( + padding: DropZoneWidgetStyle.margin, + child: DottedBorder( + borderType: BorderType.RRect, + radius: const Radius.circular(DropZoneWidgetStyle.radius), + color: DropZoneWidgetStyle.borderColor, + strokeWidth: DropZoneWidgetStyle.borderWidth, + dashPattern: DropZoneWidgetStyle.dashSize, + child: Container( + clipBehavior: Clip.antiAlias, + decoration: const ShapeDecoration( + color: DropZoneWidgetStyle.backgroundColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(DropZoneWidgetStyle.radius)), + ), + ), + width: widget.width, + height: widget.height, + padding: DropZoneWidgetStyle.padding, + alignment: AlignmentDirectional.center, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset(_imagePaths.icDropZoneIcon), + const SizedBox(height: DropZoneWidgetStyle.space), + Text( + AppLocalizations.of(context).dropFileHereToAttachThem, + style: DropZoneWidgetStyle.labelTextStyle, + ) + ] + ), + ), + ), + ); + } else { + return SizedBox(width: widget.width, height: widget.height); + } + }, + onAccept: widget.addAttachmentFromDropZone, + onLeave: (attachment) { + if (_isDragging) { + setState(() => _isDragging = false); + } + }, + onMove: (details) { + if (!_isDragging) { + setState(() => _isDragging = true); + } + }, + ); + } +} diff --git a/lib/features/composer/presentation/widgets/web/minimize_composer_widget.dart b/lib/features/composer/presentation/widgets/web/minimize_composer_widget.dart new file mode 100644 index 0000000000..6aa9d61fcc --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/minimize_composer_widget.dart @@ -0,0 +1,90 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/screen_display_mode.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/minimize_composer_widget_style.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/title_composer_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnChangeDisplayModeAction = Function(ScreenDisplayMode mode); + +class MinimizeComposerWidget extends StatelessWidget { + + final OnChangeDisplayModeAction onChangeDisplayModeAction; + final VoidCallback onCloseViewAction; + final String emailSubject; + + final _imagePaths = Get.find(); + + MinimizeComposerWidget({ + super.key, + required this.emailSubject, + required this.onChangeDisplayModeAction, + required this.onCloseViewAction, + }); + + @override + Widget build(BuildContext context) { + return Card( + elevation: MinimizeComposerWidgetStyle.elevation, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(MinimizeComposerWidgetStyle.radius)) + ), + clipBehavior: Clip.antiAlias, + child: Container( + color: MinimizeComposerWidgetStyle.backgroundColor, + width: MinimizeComposerWidgetStyle.width, + height: MinimizeComposerWidgetStyle.height, + padding: MinimizeComposerWidgetStyle.padding, + child: Stack( + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: MinimizeComposerWidgetStyle.width / 2), + child: TitleComposerWidget(emailSubject: emailSubject), + ), + ), + Align( + alignment: AlignmentDirectional.centerStart, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + TMailButtonWidget.fromIcon( + icon: _imagePaths.icCancel, + backgroundColor: Colors.transparent, + tooltipMessage: AppLocalizations.of(context).saveAndClose, + iconSize: MinimizeComposerWidgetStyle.iconSize, + iconColor: MinimizeComposerWidgetStyle.iconColor, + padding: MinimizeComposerWidgetStyle.iconPadding, + onTapActionCallback: onCloseViewAction + ), + const SizedBox(width: MinimizeComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icFullScreen, + backgroundColor: Colors.transparent, + tooltipMessage: AppLocalizations.of(context).fullscreen, + iconSize: MinimizeComposerWidgetStyle.iconSize, + iconColor: MinimizeComposerWidgetStyle.iconColor, + padding: MinimizeComposerWidgetStyle.iconPadding, + onTapActionCallback: () => onChangeDisplayModeAction(ScreenDisplayMode.fullScreen) + ), + const SizedBox(width: MinimizeComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icChevronUp, + backgroundColor: Colors.transparent, + tooltipMessage: AppLocalizations.of(context).show, + iconSize: MinimizeComposerWidgetStyle.iconSize, + iconColor: MinimizeComposerWidgetStyle.iconColor, + padding: MinimizeComposerWidgetStyle.iconPadding, + onTapActionCallback: () => onChangeDisplayModeAction(ScreenDisplayMode.normal) + ), + ] + ), + ) + ], + ) + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart b/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart new file mode 100644 index 0000000000..72cd05b5c1 --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/mobile_responsive_app_bar_composer_widget.dart @@ -0,0 +1,120 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/composer/presentation/styles/mobile_app_bar_composer_widget_style.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnOpenContextMenuAction = Function(RelativeRect position); + +class MobileResponsiveAppBarComposerWidget extends StatelessWidget { + + final bool isCodeViewEnabled; + final bool isSendButtonEnabled; + final bool isFormattingOptionsEnabled; + final VoidCallback onCloseViewAction; + final VoidCallback attachFileAction; + final VoidCallback insertImageAction; + final VoidCallback sendMessageAction; + final OnOpenContextMenuAction openContextMenuAction; + final VoidCallback openRichToolbarAction; + + final _imagePaths = Get.find(); + + MobileResponsiveAppBarComposerWidget({ + super.key, + required this.isCodeViewEnabled, + required this.isFormattingOptionsEnabled, + required this.isSendButtonEnabled, + required this.openRichToolbarAction, + required this.onCloseViewAction, + required this.attachFileAction, + required this.insertImageAction, + required this.sendMessageAction, + required this.openContextMenuAction, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: MobileAppBarComposerWidgetStyle.height, + color: MobileAppBarComposerWidgetStyle.backgroundColor, + padding: MobileAppBarComposerWidgetStyle.padding, + child: Row( + children: [ + TMailButtonWidget.fromIcon( + icon: _imagePaths.icCancel, + backgroundColor: Colors.transparent, + tooltipMessage: AppLocalizations.of(context).saveAndClose, + iconSize: MobileAppBarComposerWidgetStyle.iconSize, + iconColor: MobileAppBarComposerWidgetStyle.iconColor, + padding: MobileAppBarComposerWidgetStyle.iconPadding, + onTapActionCallback: onCloseViewAction + ), + const Spacer(), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icRichToolbar, + borderRadius: MobileAppBarComposerWidgetStyle.iconRadius, + padding: MobileAppBarComposerWidgetStyle.richTextIconPadding, + backgroundColor: isFormattingOptionsEnabled + ? MobileAppBarComposerWidgetStyle.selectedBackgroundColor + : Colors.transparent, + iconSize: MobileAppBarComposerWidgetStyle.richTextIconSize, + iconColor: isFormattingOptionsEnabled + ? MobileAppBarComposerWidgetStyle.selectedIconColor + : MobileAppBarComposerWidgetStyle.iconColor, + tooltipMessage: AppLocalizations.of(context).formattingOptions, + onTapActionCallback: openRichToolbarAction, + ), + const SizedBox(width: MobileAppBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icAttachFile, + iconColor: MobileAppBarComposerWidgetStyle.iconColor, + borderRadius: MobileAppBarComposerWidgetStyle.iconRadius, + backgroundColor: Colors.transparent, + padding: MobileAppBarComposerWidgetStyle.iconPadding, + iconSize: MobileAppBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).attach_file, + onTapActionCallback: attachFileAction, + ), + const SizedBox(width: MobileAppBarComposerWidgetStyle.space), + AbsorbPointer( + absorbing: isCodeViewEnabled, + child: TMailButtonWidget.fromIcon( + icon: _imagePaths.icInsertImage, + iconColor: MobileAppBarComposerWidgetStyle.iconColor, + borderRadius: MobileAppBarComposerWidgetStyle.iconRadius, + backgroundColor: Colors.transparent, + padding: MobileAppBarComposerWidgetStyle.iconPadding, + iconSize: MobileAppBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).insertImage, + onTapActionCallback: insertImageAction, + ), + ), + const SizedBox(width: MobileAppBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: isSendButtonEnabled + ? _imagePaths.icSendMobile + : _imagePaths.icSendDisable, + backgroundColor: Colors.transparent, + padding: MobileAppBarComposerWidgetStyle.iconPadding, + iconSize: MobileAppBarComposerWidgetStyle.sendButtonIconSize, + tooltipMessage: AppLocalizations.of(context).send, + onTapActionCallback: sendMessageAction, + ), + const SizedBox(width: MobileAppBarComposerWidgetStyle.space), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icMore, + iconColor: MobileAppBarComposerWidgetStyle.iconColor, + borderRadius: MobileAppBarComposerWidgetStyle.iconRadius, + backgroundColor: Colors.transparent, + padding: MobileAppBarComposerWidgetStyle.iconPadding, + iconSize: MobileAppBarComposerWidgetStyle.iconSize, + tooltipMessage: AppLocalizations.of(context).more, + onTapActionAtPositionCallback: openContextMenuAction, + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart b/lib/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart new file mode 100644 index 0000000000..9b53cd64b6 --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart @@ -0,0 +1,232 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/base/widget/drop_down_button_widget.dart'; +import 'package:tmail_ui_user/features/base/widget/popup_menu_overlay_widget.dart'; +import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; +import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/dropdown_menu_font_status.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/font_name_type.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/header_style_type.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/order_list_type.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/paragraph_type.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/rich_text_style_type.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/drop_down_menu_header_style_widget.dart'; + +class ToolbarRichTextWebBuilder extends StatelessWidget with RichTextButtonMixin { + + final RichTextWebController richTextWebController; + final ImagePaths _imagePaths = Get.find(); + final EdgeInsetsGeometry? padding; + final List? extendedOption; + final AlignmentGeometry? alignment; + final Decoration? decoration; + + ToolbarRichTextWebBuilder({ + Key? key, + required this.richTextWebController, + this.padding, + this.extendedOption, + this.alignment, + this.decoration, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + final codeViewEnabled = richTextWebController.codeViewEnabled; + final opacity = codeViewEnabled ? 0.5 : 1.0; + + return PointerInterceptor( + child: Container( + padding: padding ?? const EdgeInsetsDirectional.only(start: 20, top: 8, bottom: 8), + decoration: decoration, + width: double.infinity, + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + runSpacing: 8, + children: [ + if (extendedOption?.isNotEmpty == true) + ...extendedOption!, + AbsorbPointer( + absorbing: codeViewEnabled, + child: DropDownMenuHeaderStyleWidget( + icon: buildWrapIconStyleText( + isSelected: richTextWebController.isMenuHeaderStyleOpen, + icon: SvgPicture.asset( + RichTextStyleType.headerStyle.getIcon(_imagePaths), + colorFilter: AppColor.colorDefaultRichTextButton.withOpacity(opacity).asFilter(), + fit: BoxFit.fill), + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), + tooltip: RichTextStyleType.headerStyle.getTooltipButton(context) + ), + items: HeaderStyleType.values, + onMenuStateChange: (isOpen) { + log('ComposerView::_buildToolbarRichTextWidget(): MenuHeaderStyleStatus: $isOpen'); + final newStatus = isOpen + ? DropdownMenuFontStatus.open + : DropdownMenuFontStatus.closed; + richTextWebController.menuHeaderStyleStatus.value = newStatus; + }, + onChanged: (newStyle) => richTextWebController.applyHeaderStyle(newStyle)), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: Container( + width: 130, + padding: const EdgeInsets.only(left: 4.0, right: 4.0), + child: DropDownButtonWidget( + items: FontNameType.values, + itemSelected: richTextWebController.selectedFontName.value, + onChanged: (newFont) => richTextWebController.applyNewFontStyle(newFont), + onMenuStateChange: (isOpen) { + log('ComposerView::_buildToolbarRichTextWidget(): MenuFontStatus: $isOpen'); + final newStatus = isOpen + ? DropdownMenuFontStatus.open + : DropdownMenuFontStatus.closed; + richTextWebController.menuFontStatus.value = newStatus; + }, + heightItem: 40, + sizeIconChecked: 16, + radiusButton: 8, + opacity: opacity, + dropdownWidth: 200, + colorButton: richTextWebController.isMenuFontOpen + ? AppColor.colorBackgroundWrapIconStyleCode + : Colors.white, + iconArrowDown: SvgPicture.asset(_imagePaths.icStyleArrowDown), + tooltip: RichTextStyleType.fontName.getTooltipButton(context), + supportSelectionIcon: true)), + ), + Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: AbsorbPointer( + absorbing: codeViewEnabled, + child: buildWrapIconStyleText( + icon: buildIconWithTooltip( + path: RichTextStyleType.textColor.getIcon(_imagePaths), + color: richTextWebController.selectedTextColor.value, + tooltip: RichTextStyleType.textColor.getTooltipButton(context), + opacity: opacity), + onTap: () => richTextWebController.applyRichTextStyle(context, RichTextStyleType.textColor)), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: AbsorbPointer( + absorbing: codeViewEnabled, + child: buildWrapIconStyleText( + padding: const EdgeInsets.symmetric(vertical: 9, horizontal: 7), + spacing: 3, + icon: buildIconColorBackgroundText( + iconData: RichTextStyleType.textBackgroundColor.getIconData(), + colorSelected: richTextWebController.selectedTextBackgroundColor.value, + tooltip: RichTextStyleType.textBackgroundColor.getTooltipButton(context), + opacity: opacity), + onTap: () => richTextWebController.applyRichTextStyle(context, RichTextStyleType.textBackgroundColor)), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: buildWrapIconStyleText( + hasDropdown: false, + padding: const EdgeInsets.symmetric(vertical: 3, horizontal: 5), + icon: Wrap(children: [ + AbsorbPointer( + absorbing: codeViewEnabled, + child: buildIconStyleText( + path: RichTextStyleType.bold.getIcon(_imagePaths), + isSelected: richTextWebController.isTextStyleTypeSelected(RichTextStyleType.bold), + tooltip: RichTextStyleType.bold.getTooltipButton(context), + opacity: opacity, + onTap: () => richTextWebController.applyRichTextStyle(context, RichTextStyleType.bold)), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: buildIconStyleText( + path: RichTextStyleType.italic.getIcon(_imagePaths), + isSelected: richTextWebController.isTextStyleTypeSelected(RichTextStyleType.italic), + tooltip: RichTextStyleType.italic.getTooltipButton(context), + opacity: opacity, + onTap: () => richTextWebController.applyRichTextStyle(context, RichTextStyleType.italic)), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: buildIconStyleText( + path: RichTextStyleType.underline.getIcon(_imagePaths), + isSelected: richTextWebController.isTextStyleTypeSelected(RichTextStyleType.underline), + tooltip: RichTextStyleType.underline.getTooltipButton(context), + opacity: opacity, + onTap: () => richTextWebController.applyRichTextStyle(context, RichTextStyleType.underline)), + ), + AbsorbPointer( + absorbing: codeViewEnabled, + child: buildIconStyleText( + path: RichTextStyleType.strikeThrough.getIcon(_imagePaths), + isSelected: richTextWebController.isTextStyleTypeSelected( + RichTextStyleType.strikeThrough), + tooltip: RichTextStyleType.strikeThrough.getTooltipButton(context), + opacity: opacity, + onTap: () => richTextWebController.applyRichTextStyle(context, RichTextStyleType.strikeThrough)), + ) + ])), + ), + Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: AbsorbPointer( + absorbing: codeViewEnabled, + child: PopupMenuOverlayWidget( + controller: richTextWebController.menuParagraphController, + listButtonAction: ParagraphType.values + .map((paragraph) => paragraph.buildButtonWidget( + context, + _imagePaths, + (paragraph) => richTextWebController.applyParagraphType(paragraph))) + .toList(), + iconButton: buildWrapIconStyleText( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), + spacing: 3, + isSelected: richTextWebController.focusMenuParagraph.value, + icon: buildIconWithTooltip( + path: richTextWebController.selectedParagraph.value.getIcon(_imagePaths), + color: AppColor.colorDefaultRichTextButton, + opacity: opacity, + tooltip: RichTextStyleType.paragraph.getTooltipButton(context))), + ), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: AbsorbPointer( + absorbing: codeViewEnabled, + child: PopupMenuOverlayWidget( + controller: richTextWebController.menuOrderListController, + listButtonAction: OrderListType.values + .map((orderType) => orderType.buildButtonWidget( + context, + _imagePaths, + (orderType) => richTextWebController.applyOrderListType(orderType))) + .toList(), + iconButton: buildWrapIconStyleText( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 5), + spacing: 3, + isSelected: richTextWebController.focusMenuOrderList.value, + icon: buildIconWithTooltip( + path: richTextWebController.selectedOrderList.value.getIcon(_imagePaths), + color: AppColor.colorDefaultRichTextButton, + opacity: opacity, + tooltip: RichTextStyleType.orderList.getTooltipButton(context))), + ), + ), + ) + ] + ), + ), + ); + }); + } +} \ No newline at end of file diff --git a/lib/features/composer/presentation/widgets/web/web_editor_widget.dart b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart new file mode 100644 index 0000000000..1cb67cbe38 --- /dev/null +++ b/lib/features/composer/presentation/widgets/web/web_editor_widget.dart @@ -0,0 +1,117 @@ + +import 'package:core/presentation/utils/html_transformer/html_utils.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:html_editor_enhanced/html_editor.dart'; + +typedef OnChangeContentEditorAction = Function(String? text); +typedef OnInitialContentEditorAction = Function(String text); +typedef OnMouseDownEditorAction = Function(BuildContext context); +typedef OnEditorSettingsChange = Function(EditorSettings settings); +typedef OnImageUploadSuccessAction = Function(FileUpload fileUpload); +typedef OnImageUploadFailureAction = Function(FileUpload? fileUpload, String? base64Str, UploadError error); + +class WebEditorWidget extends StatefulWidget { + + final String content; + final TextDirection direction; + final HtmlEditorController editorController; + final OnInitialContentEditorAction? onInitial; + final OnChangeContentEditorAction? onChangeContent; + final VoidCallback? onFocus; + final VoidCallback? onUnFocus; + final OnMouseDownEditorAction? onMouseDown; + final OnEditorSettingsChange? onEditorSettings; + final OnImageUploadSuccessAction? onImageUploadSuccessAction; + final OnImageUploadFailureAction? onImageUploadFailureAction; + final double? width; + final double? height; + + const WebEditorWidget({ + super.key, + required this.content, + required this.direction, + required this.editorController, + this.onInitial, + this.onChangeContent, + this.onFocus, + this.onUnFocus, + this.onMouseDown, + this.onEditorSettings, + this.onImageUploadSuccessAction, + this.onImageUploadFailureAction, + this.width, + this.height, + }); + + @override + State createState() => _WebEditorState(); +} + +class _WebEditorState extends State { + + static const double _offsetHeight = 50; + static const double _offsetWidth = 90; + + late HtmlEditorController _editorController; + double? dropZoneWidth; + double? dropZoneHeight; + + @override + void initState() { + super.initState(); + _editorController = widget.editorController; + if (widget.height != null) { + dropZoneHeight = widget.height! - _offsetHeight; + } + if (widget.width != null) { + dropZoneWidth = widget.width! - _offsetWidth; + } + log('_WebEditorState::initState:dropZoneWidth: $dropZoneWidth | dropZoneHeight: $dropZoneHeight'); + } + + @override + void didUpdateWidget(covariant WebEditorWidget oldWidget) { + log('_EmailEditorState::didUpdateWidget():Old: ${oldWidget.direction} | current: ${widget.direction}'); + if (oldWidget.direction != widget.direction) { + _editorController.updateBodyDirection(widget.direction.name); + } + super.didUpdateWidget(oldWidget); + } + + @override + Widget build(BuildContext context) { + return HtmlEditor( + key: const Key('web_editor'), + controller: _editorController, + htmlEditorOptions: HtmlEditorOptions( + shouldEnsureVisible: true, + hint: '', + darkMode: false, + initialText: widget.content, + customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: widget.direction), + ), + htmlToolbarOptions: const HtmlToolbarOptions( + toolbarType: ToolbarType.hide, + defaultToolbarButtons: [], + ), + otherOptions: OtherOptions( + height: 550, + dropZoneWidth: dropZoneWidth, + dropZoneHeight: dropZoneHeight, + ), + callbacks: Callbacks( + onBeforeCommand: widget.onChangeContent, + onChangeContent: widget.onChangeContent, + onInit: () => widget.onInitial?.call(widget.content), + onFocus: widget.onFocus, + onBlur: widget.onUnFocus, + onMouseDown: () => widget.onMouseDown?.call(context), + onChangeSelection: widget.onEditorSettings, + onChangeCodeview: widget.onChangeContent, + onImageUpload: widget.onImageUploadSuccessAction, + onImageUploadError: widget.onImageUploadFailureAction, + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/contact/presentation/contact_controller.dart b/lib/features/contact/presentation/contact_controller.dart index 039071953e..d55a630012 100644 --- a/lib/features/contact/presentation/contact_controller.dart +++ b/lib/features/contact/presentation/contact_controller.dart @@ -1,6 +1,7 @@ +import 'package:core/presentation/utils/keyboard_utils.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -21,8 +22,8 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class ContactController extends BaseController { - TextEditingController? textInputSearchController; - FocusNode? textInputSearchFocus; + final TextEditingController textInputSearchController = TextEditingController(); + final FocusNode textInputSearchFocus = FocusNode(); ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact; final searchQuery = SearchQuery.initial().obs; @@ -31,9 +32,9 @@ class ContactController extends BaseController { GetAutoCompleteWithDeviceContactInteractor? _getAutoCompleteWithDeviceContactInteractor; GetAutoCompleteInteractor? _getAutoCompleteInteractor; - late Debouncer _deBouncerTime; - late AccountId _accountId; - late Session _session; + final Debouncer _deBouncerTime = Debouncer(const Duration(milliseconds: 500), initialValue: ''); + AccountId? _accountId; + Session? _session; ContactArguments? arguments; EmailAddress? contactSelected; @@ -43,14 +44,19 @@ class ContactController extends BaseController { @override void onInit() { super.onInit(); - textInputSearchController = TextEditingController(); - textInputSearchFocus = FocusNode(); - _initializeDebounceTimeTextSearchChange(); + log('ContactController::onInit():arguments: ${Get.arguments}'); + arguments = Get.arguments; + _deBouncerTime.values.listen((value) { + searchQuery.value = SearchQuery(value); + _searchContactByNameOrEmail(searchQuery.value.value); + }); } @override void onReady() async { - textInputSearchFocus?.requestFocus(); + super.onReady(); + log('ContactController::onReady():'); + textInputSearchFocus.requestFocus(); if (arguments != null) { _accountId = arguments!.accountId; _session = arguments!.session; @@ -62,38 +68,34 @@ class ContactController extends BaseController { } injectAutoCompleteBindings(_session, _accountId); } - if (!BuildUtils.isWeb) { + if (PlatformInfo.isMobile) { Future.delayed( const Duration(milliseconds: 500), () => _checkContactPermission()); } - super.onReady(); } @override void onClose() { - _disposeWidget(); + log('ContactController::onClose():'); + textInputSearchFocus.dispose(); + textInputSearchController.dispose(); + _deBouncerTime.cancel(); super.onClose(); } - void _initializeDebounceTimeTextSearchChange() { - _deBouncerTime = Debouncer( - const Duration(milliseconds: 500), - initialValue: ''); - _deBouncerTime.values.listen((value) async { - searchQuery.value = SearchQuery(value); - _searchContactByNameOrEmail(searchQuery.value.value); - }); + void onTextSearchChange(String text) { + _deBouncerTime.value = text; } - void onTextSearchChange(String text) { + void onSearchTextAction(String text) { _deBouncerTime.value = text; } void clearAllTextInputSearchForm() { - textInputSearchController?.clear(); + textInputSearchController.clear(); searchQuery.value = SearchQuery.initial(); - textInputSearchFocus?.requestFocus(); + textInputSearchFocus.requestFocus(); } void _checkContactPermission() async { @@ -162,37 +164,14 @@ class ContactController extends BaseController { } } - void _disposeWidget() { - textInputSearchFocus?.dispose(); - textInputSearchFocus = null; - textInputSearchController?.dispose(); - textInputSearchController = null; - _deBouncerTime.cancel(); - } - void selectContact(BuildContext context, EmailAddress emailAddress) { - FocusScope.of(context).unfocus(); - - if (BuildUtils.isWeb) { - _disposeWidget(); - onSelectedContactCallback?.call(emailAddress); - } else { - popBack(result: emailAddress); - } + KeyboardUtils.hideKeyboard(context); + popBack(result: emailAddress); } void closeContactView(BuildContext context) { clearAllTextInputSearchForm(); - FocusScope.of(context).unfocus(); - - if (BuildUtils.isWeb) { - _disposeWidget(); - onDismissContactView?.call(); - } else { - popBack(); - } + KeyboardUtils.hideKeyboard(context); + popBack(); } - - @override - void onDone() {} } \ No newline at end of file diff --git a/lib/features/contact/presentation/contact_view.dart b/lib/features/contact/presentation/contact_view.dart index 16867f87b0..b8c5a85aef 100644 --- a/lib/features/contact/presentation/contact_view.dart +++ b/lib/features/contact/presentation/contact_view.dart @@ -6,9 +6,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/suggestion_email_address.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/suggestion_email_address.dart'; import 'package:tmail_ui_user/features/contact/presentation/contact_controller.dart'; -import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/contact/presentation/utils/contact_utils.dart'; import 'package:tmail_ui_user/features/contact/presentation/widgets/app_bar_contact_widget.dart'; import 'package:tmail_ui_user/features/contact/presentation/widgets/contact_suggestion_box_item.dart'; @@ -20,23 +19,7 @@ class ContactView extends GetWidget { final _responsiveUtils = Get.find(); final _imagePaths = Get.find(); - @override - final controller = Get.find(); - - ContactView({Key? key}) : super(key: key) { - controller.arguments = Get.arguments; - } - - ContactView.fromArguments( - ContactArguments arguments, { - Key? key, - SelectedContactCallbackAction? onSelectedContactCallback, - VoidCallback? onDismissCallback - }) : super(key: key) { - controller.arguments = arguments; - controller.onSelectedContactCallback = onSelectedContactCallback; - controller.onDismissContactView = onDismissCallback; - } + ContactView({super.key}); @override Widget build(BuildContext context) { @@ -78,29 +61,29 @@ class ContactView extends GetWidget { onCloseContactView: () => controller.closeContactView(context)) ), const Divider(color: AppColor.colorDividerComposer, height: 1), - (SearchAppBarWidget( - _imagePaths, - controller.searchQuery.value, - controller.textInputSearchFocus, - controller.textInputSearchController, - hasBackButton: false, - hasSearchButton: true) - ..addPadding(EdgeInsets.zero) - ..setHeightSearchBar(44) - ..setMargin(ContactUtils.getPaddingSearchInputForm(context, _responsiveUtils)) - ..addDecoration(BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: AppColor.colorBgSearchBar)) - ..addIconClearText(SvgPicture.asset( - _imagePaths.icClearTextSearch, - width: 18, - height: 18, - fit: BoxFit.fill)) - ..setHintText(AppLocalizations.of(context).hintSearchInputContact) - ..addOnClearTextSearchAction(() => controller.clearAllTextInputSearchForm()) - ..addOnTextChangeSearchAction(controller.onTextSearchChange) - ..addOnSearchTextAction((query) => {})) - .build(), + SearchAppBarWidget( + imagePaths: _imagePaths, + searchQuery: controller.searchQuery.value, + searchFocusNode: controller.textInputSearchFocus, + searchInputController: controller.textInputSearchController, + hasBackButton: false, + hasSearchButton: true, + padding: EdgeInsets.zero, + heightSearchBar: 44, + margin: ContactUtils.getPaddingSearchInputForm(context, _responsiveUtils), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + color: AppColor.colorBgSearchBar), + iconClearText: SvgPicture.asset( + _imagePaths.icClearTextSearch, + width: 18, + height: 18, + fit: BoxFit.fill), + hintText: AppLocalizations.of(context).hintSearchInputContact, + onClearTextSearchAction: controller.clearAllTextInputSearchForm, + onTextChangeSearchAction: controller.onTextSearchChange, + onSearchTextAction: controller.onSearchTextAction, + ), Expanded(child: Obx(() { if (controller.listContactSearched.isNotEmpty) { return Container( diff --git a/lib/features/contact/presentation/utils/contact_utils.dart b/lib/features/contact/presentation/utils/contact_utils.dart index d44ab9eb5a..214731eefc 100644 --- a/lib/features/contact/presentation/utils/contact_utils.dart +++ b/lib/features/contact/presentation/utils/contact_utils.dart @@ -1,11 +1,11 @@ import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; class ContactUtils { static EdgeInsets getPaddingAppBar(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return const EdgeInsets.symmetric(horizontal: 16); } else { if (responsiveUtils.isScreenWithShortestSide(context)) { @@ -17,7 +17,7 @@ class ContactUtils { } static EdgeInsets getPaddingSearchInputForm(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return const EdgeInsets.symmetric(horizontal: 16, vertical: 10); } else { if (responsiveUtils.isScreenWithShortestSide(context)) { @@ -29,7 +29,7 @@ class ContactUtils { } static EdgeInsets getPaddingSearchResultList(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return const EdgeInsets.symmetric(horizontal: 16, vertical: 10); } else { if (responsiveUtils.isScreenWithShortestSide(context)) { @@ -41,7 +41,7 @@ class ContactUtils { } static EdgeInsets getPaddingDividerSearchResultList(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return const EdgeInsets.symmetric(horizontal: 16); } else { if (responsiveUtils.isScreenWithShortestSide(context)) { @@ -53,7 +53,7 @@ class ContactUtils { } static bool supportAppBarTopBorder(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb || responsiveUtils.isLandscapeMobile(context)) { + if (PlatformInfo.isWeb || responsiveUtils.isLandscapeMobile(context)) { return false; } return true; diff --git a/lib/features/contact/presentation/widgets/app_bar_contact_widget.dart b/lib/features/contact/presentation/widgets/app_bar_contact_widget.dart index 0a8b832ead..de3a01ace4 100644 --- a/lib/features/contact/presentation/widgets/app_bar_contact_widget.dart +++ b/lib/features/contact/presentation/widgets/app_bar_contact_widget.dart @@ -24,11 +24,12 @@ class AppBarContactWidget extends StatelessWidget { Positioned( left: 0, child: buildIconWeb( - icon: SvgPicture.asset(_imagePaths.icCloseComposer, - color: AppColor.colorCloseButton, - width: 24, - height: 24, - fit: BoxFit.fill), + icon: SvgPicture.asset( + _imagePaths.icClose, + colorFilter: AppColor.colorCloseButton.asFilter(), + width: 24, + height: 24, + fit: BoxFit.fill), minSize: 25, iconSize: 25, iconPadding: const EdgeInsets.all(5), diff --git a/lib/features/contact/presentation/widgets/autocomplete_contact_text_field_with_tags.dart b/lib/features/contact/presentation/widgets/autocomplete_contact_text_field_with_tags.dart index dc8d352b86..31231304c1 100644 --- a/lib/features/contact/presentation/widgets/autocomplete_contact_text_field_with_tags.dart +++ b/lib/features/contact/presentation/widgets/autocomplete_contact_text_field_with_tags.dart @@ -5,23 +5,24 @@ import 'package:collection/collection.dart'; import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/presentation/views/button/button_builder.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/model.dart'; import 'package:super_tag_editor/tag_editor.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/suggestion_email_address.dart'; +import 'package:tmail_ui_user/features/base/widget/material_text_icon_button.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/suggestion_email_address.dart'; import 'package:tmail_ui_user/features/contact/presentation/widgets/contact_input_tag_item.dart'; import 'package:tmail_ui_user/features/contact/presentation/widgets/contact_suggestion_box_item.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; typedef OnSuggestionContactCallbackAction = Future> Function(String query); typedef OnAddListContactCallbackAction = Function(List listEmailAddress); -typedef OnExceptionAddListContactCallbackAction = Function(); +typedef OnExceptionAddListContactCallbackAction = Function(bool isListEmpty); class AutocompleteContactTextFieldWithTags extends StatefulWidget { @@ -49,6 +50,7 @@ class _AutocompleteContactTextFieldWithTagsState extends State(); final _imagePaths = Get.find(); + final GlobalKey keyToEmailTagEditor = GlobalKey(); late List listEmailAddress; @@ -70,6 +72,7 @@ class _AutocompleteContactTextFieldWithTagsState extends State( + key: keyToEmailTagEditor, length: listEmailAddress.length, controller: widget.controller, borderRadius: 12, @@ -83,7 +86,7 @@ class _AutocompleteContactTextFieldWithTagsState extends State addedEmailAddress) { - return addedEmailAddress.every((addedMail) => addedMail.emailAddress.isEmail); + return addedEmailAddress.every((addedMail) => addedMail.emailAddress.isEmail || AppUtils.isEmailLocalhost(addedMail.emailAddress)); } bool _inputFieldIsEmpty() { @@ -262,33 +265,56 @@ class _AutocompleteContactTextFieldWithTagsState extends State deleteContactCallbackAction?.call(contact), ); - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return Padding( padding: const EdgeInsets.only(top: 10), child: itemChild @@ -84,7 +88,7 @@ class ContactInputTagItem extends StatelessWidget { } } - bool _isValidEmailAddress(String value) => value.isEmail; + bool _isValidEmailAddress(String value) => value.isEmail || AppUtils.isEmailLocalhost(value); Color _getTagBackgroundColor() { if (lastTagFocused && isLastContact) { diff --git a/lib/features/contact/presentation/widgets/contact_suggestion_box_item.dart b/lib/features/contact/presentation/widgets/contact_suggestion_box_item.dart index c0f177f2fa..4d94c1ad69 100644 --- a/lib/features/contact/presentation/widgets/contact_suggestion_box_item.dart +++ b/lib/features/contact/presentation/widgets/contact_suggestion_box_item.dart @@ -2,13 +2,13 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/avatar/gradient_circle_avatar_icon.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/suggestion_email_address.dart'; -import 'package:tmail_ui_user/features/contact/presentation/widgets/gradient_color_avatar_icon.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/suggestion_email_address.dart'; typedef SelectedContactCallbackAction = Function(EmailAddress contactSelected); @@ -31,8 +31,8 @@ class ContactSuggestionBoxItem extends StatelessWidget { final imagePaths = Get.find(); final itemChild = Row(children: [ - GradientColorAvatarIcon( - suggestionEmailAddress.emailAddress.avatarColors, + GradientCircleAvatarIcon( + colors: suggestionEmailAddress.emailAddress.avatarColors, label: suggestionEmailAddress.emailAddress.labelAvatar, ), const SizedBox(width: 12), diff --git a/lib/features/destination_picker/presentation/destination_picker_bindings.dart b/lib/features/destination_picker/presentation/destination_picker_bindings.dart index f31cdf1962..33012ec861 100644 --- a/lib/features/destination_picker/presentation/destination_picker_bindings.dart +++ b/lib/features/destination_picker/presentation/destination_picker_bindings.dart @@ -1,7 +1,7 @@ import 'package:core/core.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_controller.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; @@ -104,8 +104,4 @@ class DestinationPickerBindings extends BaseBindings { Get.find(), )); } - - void dispose() { - Get.delete(); - } } \ No newline at end of file diff --git a/lib/features/destination_picker/presentation/destination_picker_controller.dart b/lib/features/destination_picker/presentation/destination_picker_controller.dart index 9362fa5aee..4e3e49af34 100644 --- a/lib/features/destination_picker/presentation/destination_picker_controller.dart +++ b/lib/features/destination_picker/presentation/destination_picker_controller.dart @@ -1,14 +1,10 @@ -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; -import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/utils/build_utils.dart'; -import 'package:dartz/dartz.dart'; +import 'package:core/presentation/utils/keyboard_utils.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; @@ -16,8 +12,6 @@ import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -import 'package:model/mailbox/expand_mode.dart'; -import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; @@ -31,9 +25,9 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_mailbo import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/search_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories_expand_mode.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/duplicate_name_validator.dart'; @@ -48,14 +42,10 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:uuid/uuid.dart'; -typedef OnSelectedMailboxCallback = Function(PresentationMailbox? destinationMailbox); - class DestinationPickerController extends BaseMailboxController { final _uuid = Get.find(); final _appToast = Get.find(); - final _imagePaths = Get.find(); - final _responsiveUtils = Get.find(); final SearchMailboxInteractor _searchMailboxInteractor; final CreateNewMailboxInteractor _createNewMailboxInteractor; @@ -64,23 +54,21 @@ class DestinationPickerController extends BaseMailboxController { final listMailboxSearched = [].obs; final searchState = SearchState.initial().obs; final searchQuery = SearchQuery.initial().obs; - final mailboxCategoriesExpandMode = MailboxCategoriesExpandMode.initial().obs; final destinationScreenType = DestinationScreenType.destinationPicker.obs; final mailboxDestination = Rxn(); final newNameMailbox = Rxn(); - FocusNode? nameInputFocusNode; - TextEditingController? nameInputController; - TextEditingController? searchInputController; - FocusNode? searchFocus; DestinationPickerArguments? arguments; Session? _session; AccountId? accountId; MailboxId? mailboxIdSelected; - OnSelectedMailboxCallback? onSelectedMailboxCallback; - VoidCallback? onDismissDestinationPicker; List listMailboxNameAsStringExist = []; + final FocusNode nameInputFocusNode = FocusNode(); + final FocusNode searchFocus = FocusNode(); + final TextEditingController nameInputController = TextEditingController(); + final TextEditingController searchInputController = TextEditingController(); + final destinationListScrollController = ScrollController(); DestinationPickerController( this._searchMailboxInteractor, @@ -99,17 +87,14 @@ class DestinationPickerController extends BaseMailboxController { @override void onInit() { super.onInit(); - nameInputFocusNode = FocusNode(); - nameInputController = TextEditingController(); - searchInputController = TextEditingController(); - searchFocus = FocusNode(); - searchFocus?.unfocus(); - nameInputFocusNode?.unfocus(); + log('DestinationPickerController::onInit():arguments: ${Get.arguments}'); + arguments = Get.arguments; } @override void onReady() { super.onReady(); + log('DestinationPickerController::onReady():'); if (arguments != null) { mailboxAction.value = arguments!.mailboxAction; mailboxIdSelected = arguments!.mailboxIdSelected; @@ -120,47 +105,49 @@ class DestinationPickerController extends BaseMailboxController { } @override - void onData(Either newState) { - super.onData(newState); - newState.map((success) { - if (success is GetAllMailboxSuccess) { - if (mailboxAction.value == MailboxActions.move && mailboxIdSelected != null) { - buildTree( - success.mailboxList.listSubscribedMailboxes, - mailboxIdSelected: mailboxIdSelected - ); - } else { - buildTree(success.mailboxList.listSubscribedMailboxes); - } - } else if (success is RefreshChangesAllMailboxSuccess) { - refreshTree(success.mailboxList.listSubscribedMailboxes); + void handleSuccessViewState(Success success) async { + super.handleSuccessViewState(success); + if (success is GetAllMailboxSuccess) { + if (mailboxAction.value == MailboxActions.move && mailboxIdSelected != null) { + await buildTree( + success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes, + mailboxIdSelected: mailboxIdSelected); + } else { + await buildTree(success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes); } - }); + if (currentContext != null) { + await syncAllMailboxWithDisplayName(currentContext!); + } + } else if (success is RefreshChangesAllMailboxSuccess) { + await refreshTree(success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes); + if (currentContext != null) { + await syncAllMailboxWithDisplayName(currentContext!); + } + } else if (success is SearchMailboxSuccess) { + _searchMailboxSuccess(success); + } else if (success is CreateNewMailboxSuccess) { + _createNewMailboxSuccess(success); + } } @override - void onDone() { - viewState.value.fold( - (failure) { - if (failure is SearchMailboxFailure) { - _searchMailboxFailure(failure); - } else if (failure is CreateNewMailboxFailure) { - _createNewMailboxFailure(failure); - } - }, - (success) { - if (success is SearchMailboxSuccess) { - _searchMailboxSuccess(success); - } else if (success is CreateNewMailboxSuccess) { - _createNewMailboxSuccess(success); - } - } - ); + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is SearchMailboxFailure) { + _searchMailboxFailure(failure); + } else if (failure is CreateNewMailboxFailure) { + _createNewMailboxFailure(failure); + } } @override void onClose() { - _disposeWidget(); + log('DestinationPickerController::onClose():'); + nameInputFocusNode.dispose(); + nameInputController.dispose(); + searchFocus.dispose(); + searchInputController.dispose(); + destinationListScrollController.dispose(); super.onClose(); } @@ -179,13 +166,24 @@ class DestinationPickerController extends BaseMailboxController { void toggleMailboxCategories(MailboxCategories categories) { switch(categories) { case MailboxCategories.exchange: - final newExpandMode = mailboxCategoriesExpandMode.value.defaultMailbox == ExpandMode.EXPAND ? ExpandMode.COLLAPSE : ExpandMode.EXPAND; + final newExpandMode = mailboxCategoriesExpandMode.value.defaultMailbox == ExpandMode.EXPAND + ? ExpandMode.COLLAPSE + : ExpandMode.EXPAND; mailboxCategoriesExpandMode.value.defaultMailbox = newExpandMode; mailboxCategoriesExpandMode.refresh(); break; - case MailboxCategories.personalMailboxes: - final newExpandMode = mailboxCategoriesExpandMode.value.personalMailboxes == ExpandMode.EXPAND ? ExpandMode.COLLAPSE : ExpandMode.EXPAND; - mailboxCategoriesExpandMode.value.personalMailboxes = newExpandMode; + case MailboxCategories.personalFolders: + final newExpandMode = mailboxCategoriesExpandMode.value.personalFolders == ExpandMode.EXPAND + ? ExpandMode.COLLAPSE + : ExpandMode.EXPAND; + mailboxCategoriesExpandMode.value.personalFolders = newExpandMode; + mailboxCategoriesExpandMode.refresh(); + break; + case MailboxCategories.teamMailboxes: + final newExpandMode = mailboxCategoriesExpandMode.value.teamMailboxes == ExpandMode.EXPAND + ? ExpandMode.COLLAPSE + : ExpandMode.EXPAND; + mailboxCategoriesExpandMode.value.teamMailboxes = newExpandMode; mailboxCategoriesExpandMode.refresh(); break; default: @@ -204,7 +202,7 @@ class DestinationPickerController extends BaseMailboxController { String? getErrorInputNameString(BuildContext context) { final nameMailbox = newNameMailbox.value; - if (nameInputFocusNode?.hasFocus == false && nameMailbox == null) { + if (nameInputFocusNode.hasFocus == false && nameMailbox == null) { return null; } @@ -229,7 +227,7 @@ class DestinationPickerController extends BaseMailboxController { bool isCreateMailboxValidated(BuildContext context) { final nameValidated = getErrorInputNameString(context); - if (nameInputFocusNode?.hasFocus == false && newNameMailbox.value == null) { + if (nameInputFocusNode.hasFocus == false && newNameMailbox.value == null) { return false; } @@ -270,22 +268,27 @@ class DestinationPickerController extends BaseMailboxController { listMailboxSearched.clear(); searchState.value = searchState.value.disableSearchState(); searchQuery.value = SearchQuery.initial(); - searchInputController?.clear(); - FocusScope.of(context).unfocus(); + searchInputController.clear(); + KeyboardUtils.hideKeyboard(context); } void clearSearchText() { searchQuery.value = SearchQuery.initial(); - searchFocus?.requestFocus(); + searchFocus.requestFocus(); listMailboxSearched.clear(); } - void searchMailbox(String value) { + void searchMailbox(BuildContext context, String value) { searchQuery.value = SearchQuery(value); - _searchMailboxAction( - allMailboxes.listPersonalMailboxes, - searchQuery.value - ); + final searchableMailboxList = mailboxAction.value == MailboxActions.moveEmail + ? allMailboxes + : allMailboxes.listPersonalMailboxes; + + final mailboxListWithDisplayName = searchableMailboxList + .map((mailbox) => mailbox.withDisplayName(mailbox.getDisplayName(context))) + .toList(); + + _searchMailboxAction(mailboxListWithDisplayName, searchQuery.value); } void _searchMailboxAction(List allMailboxes, SearchQuery searchQuery) { @@ -307,19 +310,9 @@ class DestinationPickerController extends BaseMailboxController { void openCreateNewMailboxView(BuildContext context) async { if (mailboxDestination.value == null) { - _appToast.showBottomToast( + _appToast.showToastErrorMessage( currentOverlayContext!, - AppLocalizations.of(context).toastMessageErrorNotSelectedFolderWhenCreateNewMailbox, - leadingIcon: SvgPicture.asset( - _imagePaths.icNotConnection, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!)); + AppLocalizations.of(context).toastMessageErrorNotSelectedFolderWhenCreateNewMailbox); } else { destinationScreenType.value = DestinationScreenType.createNewMailbox; _createListMailboxNameAsStringInMailboxLocation(); @@ -327,10 +320,11 @@ class DestinationPickerController extends BaseMailboxController { } void _dispatchCreateNewMailboxFolder( - AccountId accountId, - CreateNewMailboxRequest request + Session session, + AccountId accountId, + CreateNewMailboxRequest request ) async { - consumeState(_createNewMailboxInteractor.execute(accountId, request)); + consumeState(_createNewMailboxInteractor.execute(session, accountId, request)); } void _createNewMailboxSuccess(CreateNewMailboxSuccess success) { @@ -352,20 +346,7 @@ class DestinationPickerController extends BaseMailboxController { if (exception is ErrorMethodResponse) { messageError = exception.description ?? AppLocalizations.of(currentContext!).create_new_mailbox_failure; } - - _appToast.showBottomToast( - currentOverlayContext!, - messageError, - leadingIcon: SvgPicture.asset( - _imagePaths.icNotConnection, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!)); + _appToast.showToastErrorMessage(currentOverlayContext!, messageError); } } @@ -395,35 +376,19 @@ class DestinationPickerController extends BaseMailboxController { } void dispatchSelectMailboxDestination(BuildContext context) { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); if (mailboxDestination.value == null) { - _appToast.showBottomToast( - currentOverlayContext!, - AppLocalizations.of(context).toastMessageErrorNotSelectedFolderWhenCreateNewMailbox, - leadingIcon: SvgPicture.asset( - _imagePaths.icNotConnection, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!)); + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(context).toastMessageErrorNotSelectedFolderWhenCreateNewMailbox); return; } - - if (BuildUtils.isWeb) { - _disposeWidget(); - onSelectedMailboxCallback?.call(mailboxDestination.value); - } else { - popBack(result: mailboxDestination.value); - } + popBack(result: mailboxDestination.value); } void createNewMailboxAction(BuildContext context) { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); final nameMailbox = newNameMailbox.value; if (nameMailbox != null && nameMailbox.isNotEmpty) { @@ -433,6 +398,7 @@ class DestinationPickerController extends BaseMailboxController { : mailboxDestination.value?.id; _dispatchCreateNewMailboxFolder( + _session!, accountId!, CreateNewMailboxRequest( generateCreateId, @@ -444,30 +410,13 @@ class DestinationPickerController extends BaseMailboxController { } void backToDestinationScreen(BuildContext context) { - nameInputController?.clear(); - FocusScope.of(context).unfocus(); + nameInputController.clear(); + KeyboardUtils.hideKeyboard(context); destinationScreenType.value = DestinationScreenType.destinationPicker; } - void _disposeWidget() { - nameInputFocusNode?.dispose(); - nameInputFocusNode = null; - nameInputController?.dispose(); - nameInputController = null; - searchFocus?.dispose(); - searchFocus = null; - searchInputController?.dispose(); - searchInputController = null; - } - void closeDestinationPicker(BuildContext context) { - FocusScope.of(context).unfocus(); - - if (BuildUtils.isWeb) { - _disposeWidget(); - onDismissDestinationPicker?.call(); - } else { - popBack(); - } + KeyboardUtils.hideKeyboard(context); + popBack(); } } \ No newline at end of file diff --git a/lib/features/destination_picker/presentation/destination_picker_view.dart b/lib/features/destination_picker/presentation/destination_picker_view.dart index cb3a40945b..fa14bdd23d 100644 --- a/lib/features/destination_picker/presentation/destination_picker_view.dart +++ b/lib/features/destination_picker/presentation/destination_picker_view.dart @@ -7,7 +7,8 @@ import 'package:core/presentation/views/button/icon_button_web.dart'; import 'package:core/presentation/views/list/tree_view.dart'; import 'package:core/presentation/views/search/search_bar_view.dart'; import 'package:core/presentation/views/text/text_field_builder.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -18,7 +19,6 @@ import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_controller.dart'; -import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_screen_type.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/widgets/destination_picker_search_mailbox_item_builder.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/widgets/top_bar_destination_picker_builder.dart'; @@ -27,10 +27,11 @@ import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_action import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_displayed.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_item_widget.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/widgets/create_mailbox_name_input_decoration_builder.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/search_app_bar_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; class DestinationPickerView extends GetWidget with AppLoaderMixin, @@ -43,20 +44,7 @@ class DestinationPickerView extends GetWidget @override final controller = Get.find(); - DestinationPickerView({Key? key}) : super(key: key) { - controller.arguments = Get.arguments; - } - - DestinationPickerView.fromArguments( - DestinationPickerArguments arguments, { - Key? key, - OnSelectedMailboxCallback? onSelectedMailboxCallback, - VoidCallback? onDismissCallback - }) : super(key: key) { - controller.arguments = arguments; - controller.onSelectedMailboxCallback = onSelectedMailboxCallback; - controller.onDismissDestinationPicker = onDismissCallback; - } + DestinationPickerView({super.key}); @override Widget build(BuildContext context) { @@ -71,7 +59,7 @@ class DestinationPickerView extends GetWidget borderOnForeground: false, color: Colors.transparent, child: SafeArea( - top: !BuildUtils.isWeb && _responsiveUtils.isPortraitMobile(context), + top: PlatformInfo.isMobile && _responsiveUtils.isPortraitMobile(context), bottom: false, left: false, right: false, @@ -102,8 +90,8 @@ class DestinationPickerView extends GetWidget child: SafeArea( top: false, bottom: false, - left: !BuildUtils.isWeb && _responsiveUtils.isLandscapeMobile(context), - right: !BuildUtils.isWeb && _responsiveUtils.isLandscapeMobile(context), + left: PlatformInfo.isMobile && _responsiveUtils.isLandscapeMobile(context), + right: PlatformInfo.isMobile && _responsiveUtils.isLandscapeMobile(context), child: Column(children: [ Obx(() => TopBarDestinationPickerBuilder( controller.mailboxAction.value, @@ -170,23 +158,24 @@ class DestinationPickerView extends GetWidget Widget _buildCreateMailboxNameInput(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Obx(() => (TextFieldBuilder() - ..key(const Key('create_mailbox_name_input')) - ..onChange((value) => controller.setNewNameMailbox(value)) - ..keyboardType(TextInputType.visiblePassword) - ..cursorColor(AppColor.colorTextButton) - ..addController(controller.nameInputController) - ..maxLines(1) - ..textStyle(const TextStyle( - color: AppColor.colorNameEmail, - fontSize: 16, - overflow: CommonTextStyle.defaultTextOverFlow)) - ..addFocusNode(controller.nameInputFocusNode) - ..textDecoration((CreateMailboxNameInputDecorationBuilder() - ..setHintText(AppLocalizations.of(context).hint_input_create_new_mailbox) - ..setErrorText(controller.getErrorInputNameString(context))) - .build())) - .build()) + child: Obx(() => TextFieldBuilder( + key: const Key('create_mailbox_name_input'), + onTextChange: controller.setNewNameMailbox, + keyboardType: TextInputType.visiblePassword, + cursorColor: AppColor.colorTextButton, + controller: controller.nameInputController, + textDirection: DirectionUtils.getDirectionByLanguage(context), + maxLines: 1, + textStyle: const TextStyle( + color: AppColor.colorNameEmail, + fontSize: 16, + overflow: CommonTextStyle.defaultTextOverFlow), + focusNode: controller.nameInputFocusNode, + decoration: (CreateMailboxNameInputDecorationBuilder() + ..setHintText(AppLocalizations.of(context).hint_input_create_new_mailbox) + ..setErrorText(controller.getErrorInputNameString(context))) + .build(), + )) ); } @@ -207,6 +196,7 @@ class DestinationPickerView extends GetWidget ) { return SingleChildScrollView( physics: const ClampingScrollPhysics(), + controller: controller.destinationListScrollController, child: Column(children: [ if (actions?.canSearch() == true && controller.destinationScreenType.value == DestinationScreenType.destinationPicker) @@ -240,7 +230,7 @@ class DestinationPickerView extends GetWidget const SizedBox(height: 8), _buildMailboxCategory( context, - MailboxCategories.personalMailboxes, + MailboxCategories.personalFolders, controller.personalRootNode, actions, mailboxIdSelected @@ -251,6 +241,31 @@ class DestinationPickerView extends GetWidget return const SizedBox.shrink(); } }), + Obx(() { + if (controller.teamMailboxesIsNotEmpty + && controller.mailboxAction.value == MailboxActions.moveEmail) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Divider( + color: AppColor.colorDividerMailbox, + height: 0.5, + thickness: 0.2 + ), + const SizedBox(height: 8), + _buildMailboxCategory( + context, + MailboxCategories.teamMailboxes, + controller.teamMailboxesRootNode, + actions, + mailboxIdSelected + ) + ] + ); + } else { + return const SizedBox.shrink(); + } + }), const SizedBox(height: 12) ]) ); @@ -343,34 +358,30 @@ class DestinationPickerView extends GetWidget context, key: const Key('children_tree_mailbox_child'), isExpanded: mailboxNode.expandMode == ExpandMode.EXPAND, - parent: (MailBoxFolderTileBuilder( - context, - _imagePaths, - mailboxNode, - lastNode: lastNode, - mailboxActions: actions, - mailboxIdAlreadySelected: mailboxIdSelected, - mailboxDisplayed: MailboxDisplayed.destinationPicker) - ..addOnClickOpenMailboxNodeAction((node) => _pickMailboxNode(context, node)) - ..addOnClickExpandMailboxNodeAction((mailboxNode) => controller.toggleMailboxFolder(mailboxNode)) - ).build(), + paddingChild: const EdgeInsetsDirectional.only(start: 14), + parent: MailboxItemWidget( + mailboxNode: mailboxNode, + mailboxActions: actions, + mailboxIdAlreadySelected: mailboxIdSelected, + mailboxDisplayed: MailboxDisplayed.destinationPicker, + onOpenMailboxFolderClick: (node) => _pickMailboxNode(context, node), + onExpandFolderActionClick: (mailboxNode) => controller.toggleMailboxFolder(mailboxNode, controller.destinationListScrollController), + ), children: _buildListChildTileWidget( - context, - mailboxNode, - mailboxIdSelected, - actions: actions) + context, + mailboxNode, + mailboxIdSelected, + actions: actions + ) ).build(); } else { - return (MailBoxFolderTileBuilder( - context, - _imagePaths, - mailboxNode, - lastNode: lastNode, - mailboxDisplayed: MailboxDisplayed.destinationPicker, - mailboxIdAlreadySelected: mailboxIdSelected, - mailboxActions: actions) - ..addOnClickOpenMailboxNodeAction((node) => _pickMailboxNode(context, node)) - ).build(); + return MailboxItemWidget( + mailboxNode: mailboxNode, + mailboxDisplayed: MailboxDisplayed.destinationPicker, + mailboxIdAlreadySelected: mailboxIdSelected, + mailboxActions: actions, + onOpenMailboxFolderClick: (node) => _pickMailboxNode(context, node), + ); }}) .toList() ?? []; } @@ -433,8 +444,8 @@ class DestinationPickerView extends GetWidget child: Row(children: [ SvgPicture.asset( _imagePaths.icFolderMailbox, - width: BuildUtils.isWeb ? 20 : 24, - height: BuildUtils.isWeb ? 20 : 24, + width: PlatformInfo.isWeb ? 20 : 24, + height: PlatformInfo.isWeb ? 20 : 24, fit: BoxFit.fill ), const SizedBox(width: 8), @@ -453,7 +464,10 @@ class DestinationPickerView extends GetWidget if (actions == MailboxActions.select && (mailboxIdSelected == null || mailboxIdSelected == PresentationMailbox.unifiedMailbox.id)) Padding( - padding: const EdgeInsets.only(right: 30.0), + padding: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) ? 0 : 30.0, + left: AppUtils.isDirectionRTL(context) ? 30 : 0.0, + ), child: SvgPicture.asset( _imagePaths.icFilterSelected, width: 20, @@ -507,32 +521,48 @@ class DestinationPickerView extends GetWidget padding: const EdgeInsets.symmetric(vertical: 16), child: Row( children: [ - Padding(padding: const EdgeInsets.only(left: 5), child: buildIconWeb( - icon: SvgPicture.asset(_imagePaths.icBack, color: AppColor.colorTextButton, fit: BoxFit.fill), + Padding( + padding: EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 0 : 5, + right: AppUtils.isDirectionRTL(context) ? 5 : 0, + ), + child: buildIconWeb( + icon: SvgPicture.asset( + _imagePaths.icBack, + colorFilter: AppColor.colorTextButton.asFilter(), + fit: BoxFit.fill), onTap: () => controller.disableSearch(context))), - Expanded(child: (SearchAppBarWidget( - _imagePaths, - controller.searchQuery.value, - controller.searchFocus, - controller.searchInputController, - hasBackButton: false, - hasSearchButton: true) - ..addPadding(EdgeInsets.zero) - ..setMargin(const EdgeInsets.only(right: 16)) - ..addDecoration(BoxDecoration(borderRadius: BorderRadius.circular(12), color: AppColor.colorBgSearchBar)) - ..addIconClearText(SvgPicture.asset(_imagePaths.icClearTextSearch, width: 18, height: 18, fit: BoxFit.fill)) - ..setHintText(AppLocalizations.of(context).hint_search_mailboxes) - ..addOnClearTextSearchAction(() => controller.clearSearchText()) - ..addOnTextChangeSearchAction((query) => controller.searchMailbox(query)) - ..addOnSearchTextAction((query) => controller.searchMailbox(query))) - .build()) + Expanded(child: SearchAppBarWidget( + imagePaths: _imagePaths, + searchQuery: controller.searchQuery.value, + searchFocusNode: controller.searchFocus, + searchInputController: controller.searchInputController, + hasBackButton: false, + hasSearchButton: true, + padding: EdgeInsets.zero, + margin: EdgeInsets.only( + right: DirectionUtils.isDirectionRTLByLanguage(context) ? 0 : 16, + left: DirectionUtils.isDirectionRTLByLanguage(context) ? 16 : 0), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(12)), + color: AppColor.colorBgSearchBar), + iconClearText: SvgPicture.asset( + _imagePaths.icClearTextSearch, + width: 18, + height: 18, + fit: BoxFit.fill), + hintText: AppLocalizations.of(context).hint_search_mailboxes, + onClearTextSearchAction: controller.clearSearchText, + onTextChangeSearchAction: (query) => controller.searchMailbox(context, query), + onSearchTextAction: (query) => controller.searchMailbox(context, query), + )) ] ) ); } BorderRadius _getRadiusDestinationPicker(BuildContext context) { - if (!BuildUtils.isWeb && _responsiveUtils.isLandscapeMobile(context)) { + if (PlatformInfo.isMobile && _responsiveUtils.isLandscapeMobile(context)) { return BorderRadius.zero; } else if (_responsiveUtils.isMobile(context)) { return const BorderRadius.only( @@ -544,7 +574,7 @@ class DestinationPickerView extends GetWidget } double _getWidthDestinationPicker(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (_responsiveUtils.isMobile(context)) { return double.infinity; } else { @@ -561,7 +591,7 @@ class DestinationPickerView extends GetWidget } double _getHeightDestinationPicker(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (_responsiveUtils.isMobile(context)) { return double.infinity; } else { @@ -587,7 +617,7 @@ class DestinationPickerView extends GetWidget } EdgeInsets _getMarginDestinationPicker(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (_responsiveUtils.isMobile(context)) { return EdgeInsets.zero; } else { diff --git a/lib/features/destination_picker/presentation/widgets/destination_picker_search_mailbox_item_builder.dart b/lib/features/destination_picker/presentation/widgets/destination_picker_search_mailbox_item_builder.dart index 033d84784f..ce7f9cc813 100644 --- a/lib/features/destination_picker/presentation/widgets/destination_picker_search_mailbox_item_builder.dart +++ b/lib/features/destination_picker/presentation/widgets/destination_picker_search_mailbox_item_builder.dart @@ -2,15 +2,17 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_method_action_define.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/utils/search_mailbox_utils.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; class DestinationPickerSearchMailboxItemBuilder extends StatelessWidget { @@ -59,12 +61,12 @@ class DestinationPickerSearchMailboxItemBuilder extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildTitleItem(), + _buildTitleItem(context), _buildSubtitleItem() ] ) )), - _buildSelectedIcon() + _buildSelectedIcon(context) ] ), ) @@ -75,7 +77,7 @@ class DestinationPickerSearchMailboxItemBuilder extends StatelessWidget { } void _onTapMailboxAction() { - if (onClickOpenMailboxAction != null) { + if (!_isSelectActionNoValid && onClickOpenMailboxAction != null) { onClickOpenMailboxAction?.call(_presentationMailbox); } } @@ -83,15 +85,15 @@ class DestinationPickerSearchMailboxItemBuilder extends StatelessWidget { Widget _buildMailboxIcon() { return SvgPicture.asset( _presentationMailbox.getMailboxIcon(_imagePaths), - width: BuildUtils.isWeb ? 20 : 24, - height: BuildUtils.isWeb ? 20 : 24, + width: PlatformInfo.isWeb ? 20 : 24, + height: PlatformInfo.isWeb ? 20 : 24, fit: BoxFit.fill ); } - Widget _buildTitleItem() { + Widget _buildTitleItem(BuildContext context) { return Text( - _presentationMailbox.name?.name ?? '', + _presentationMailbox.getDisplayName(context), maxLines: 1, overflow: CommonTextStyle.defaultTextOverFlow, softWrap: CommonTextStyle.defaultSoftWrap, @@ -117,7 +119,7 @@ class DestinationPickerSearchMailboxItemBuilder extends StatelessWidget { ); } else if (_presentationMailbox.isTeamMailboxes) { return Text( - _presentationMailbox.emailTeamMailBoxes ?? '', + _presentationMailbox.emailTeamMailBoxes, maxLines: 1, softWrap: CommonTextStyle.defaultSoftWrap, overflow: CommonTextStyle.defaultTextOverFlow, @@ -132,14 +134,15 @@ class DestinationPickerSearchMailboxItemBuilder extends StatelessWidget { } } - Widget _buildSelectedIcon() { - if (_presentationMailbox.id == mailboxIdAlreadySelected && - (mailboxActions == MailboxActions.select || - mailboxActions == MailboxActions.create)) { + Widget _buildSelectedIcon(BuildContext context) { + if (_isSelectActionNoValid) { return Padding( - padding: const EdgeInsets.only(right: 8), + padding: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) ? 0 : 8, + left: AppUtils.isDirectionRTL(context) ? 8 : 0, + ), child: SvgPicture.asset( - _imagePaths.icFilterSelected, + _imagePaths.icSelectedSB, width: 20, height: 20, fit: BoxFit.fill @@ -149,4 +152,11 @@ class DestinationPickerSearchMailboxItemBuilder extends StatelessWidget { return const SizedBox.shrink(); } } + + bool get _isSelectActionNoValid => _presentationMailbox.id == mailboxIdAlreadySelected && + ( + mailboxActions == MailboxActions.select || + mailboxActions == MailboxActions.create || + mailboxActions == MailboxActions.moveEmail + ); } \ No newline at end of file diff --git a/lib/features/destination_picker/presentation/widgets/top_bar_destination_picker_builder.dart b/lib/features/destination_picker/presentation/widgets/top_bar_destination_picker_builder.dart index 5bda8da736..192efc65cb 100644 --- a/lib/features/destination_picker/presentation/widgets/top_bar_destination_picker_builder.dart +++ b/lib/features/destination_picker/presentation/widgets/top_bar_destination_picker_builder.dart @@ -61,9 +61,9 @@ class TopBarDestinationPickerBuilder extends StatelessWidget { fontWeight: FontWeight.w700))), if (_destinationScreenType == DestinationScreenType.destinationPicker) Padding( - padding: const EdgeInsets.only(left: 8.0), + padding: const EdgeInsetsDirectional.only(start: 8), child: Align( - alignment: Alignment.centerLeft, + alignment: AlignmentDirectional.centerStart, child: buildIconWeb( iconSize: 24, colorSelected: Colors.white, @@ -76,9 +76,9 @@ class TopBarDestinationPickerBuilder extends StatelessWidget { ) else Padding( - padding: const EdgeInsets.only(left: 8.0), + padding: const EdgeInsetsDirectional.only(start: 8), child: Align( - alignment: Alignment.centerLeft, + alignment: AlignmentDirectional.centerStart, child: Material( color: Colors.transparent, child: InkWell( @@ -94,7 +94,7 @@ class TopBarDestinationPickerBuilder extends StatelessWidget { _imagePaths.icBack, width: 14, height: 14, - color: AppColor.primaryColor, + colorFilter: AppColor.primaryColor.asFilter(), fit: BoxFit.fill), const SizedBox(width: 5), Text( @@ -117,67 +117,4 @@ class TopBarDestinationPickerBuilder extends StatelessWidget { ), ); } - - Widget _buildIconCreateButton(BuildContext context) { - return buildIconWeb( - iconSize: 24, - colorSelected: Colors.white, - splashRadius: 15, - iconPadding: const EdgeInsets.all(3), - icon: SvgPicture.asset( - _imagePaths.icCreateNewFolder, - color: mailboxIdDestination != null - ? AppColor.colorTextButton - : AppColor.colorDisableMailboxCreateButton, - fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).create, - onTap: onOpenCreateNewMailboxScreenAction - ); - } - - Widget _buildDoneButton(BuildContext context) { - return Material( - color: Colors.transparent, - child: InkWell( - customBorder: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8))), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8), - child: Text( - AppLocalizations.of(context).done, - style: TextStyle( - fontSize: 15, - color: mailboxIdDestination != null - ? AppColor.colorTextButton - : AppColor.colorDisableMailboxCreateButton))), - onTap: onSelectedMailboxDestinationAction - ) - ); - } - - Widget _buildSaveButton(BuildContext context) { - return Material( - color: Colors.transparent, - child: InkWell( - customBorder: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8))), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8), - child: Text( - AppLocalizations.of(context).save, - style: TextStyle( - fontSize: 15, - color: isCreateMailboxValidated - ? AppColor.colorTextButton - : AppColor.colorDisableMailboxCreateButton) - ) - ), - onTap: isCreateMailboxValidated ? onCreateNewMailboxAction : null - ) - ); - } } \ No newline at end of file diff --git a/lib/features/email/data/datasource/calendar_event_datasource.dart b/lib/features/email/data/datasource/calendar_event_datasource.dart new file mode 100644 index 0000000000..a65dc1a9d9 --- /dev/null +++ b/lib/features/email/data/datasource/calendar_event_datasource.dart @@ -0,0 +1,11 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; + +abstract class CalendarEventDataSource { + Future>> parse(AccountId accountId, Set blobIds); + + Future> getListEventAction(String emailContents); +} \ No newline at end of file diff --git a/lib/features/email/data/datasource/email_datasource.dart b/lib/features/email/data/datasource/email_datasource.dart index 6b39c96256..e35f72c818 100644 --- a/lib/features/email/data/datasource/email_datasource.dart +++ b/lib/features/email/data/datasource/email_datasource.dart @@ -1,22 +1,38 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:core/core.dart'; +import 'package:core/data/network/download/downloaded_response.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:model/model.dart'; +import 'package:model/account/account_request.dart'; +import 'package:model/download/download_task_id.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/email/mark_star_action.dart'; +import 'package:model/email/read_actions.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; abstract class EmailDataSource { - Future getEmailContent(AccountId accountId, EmailId emailId); + Future getEmailContent(Session session, AccountId accountId, EmailId emailId); - Future sendEmail(AccountId accountId, EmailRequest emailRequest, {CreateNewMailboxRequest? mailboxRequest}); + Future sendEmail( + Session session, + AccountId accountId, + EmailRequest emailRequest, + {CreateNewMailboxRequest? mailboxRequest} + ); - Future> markAsRead(AccountId accountId, List emails, ReadActions readActions); + Future> markAsRead(Session session, AccountId accountId, List emails, ReadActions readActions); Future> downloadAttachments( List attachments, @@ -42,21 +58,50 @@ abstract class EmailDataSource { StreamController> onReceiveController ); - Future> moveToMailbox(AccountId accountId, MoveToMailboxRequest moveRequest); + Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest); Future> markAsStar( + Session session, AccountId accountId, List emails, MarkStarAction markStarAction ); - Future saveEmailAsDrafts(AccountId accountId, Email email); + Future saveEmailAsDrafts(Session session, AccountId accountId, Email email); + + Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId); + + Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId); + + Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); + + Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId); + + Future storeDetailedNewEmail(Session session, AccountId accountId, DetailedEmail detailedEmail); + + Future> getListDetailedEmailById(Session session, AccountId accountId, Set emailIds, {Set? sort}); + + Future storeEmail(Session session, AccountId accountId, Email email); + + Future getStoredEmail(Session session, AccountId accountId, EmailId emailId); + + Future storeOpenedEmail(Session session, AccountId accountId, DetailedEmail detailedEmail); + + Future getStoredOpenedEmail(Session session, AccountId accountId, EmailId emailId); + + Future getStoredNewEmail(Session session, AccountId accountId, EmailId emailId); + + Future storeSendingEmail(AccountId accountId, UserName userName, SendingEmail sendingEmail); + + Future updateSendingEmail(AccountId accountId, UserName userName, SendingEmail newSendingEmail); + + Future> getAllSendingEmails(AccountId accountId, UserName userName); - Future removeEmailDrafts(AccountId accountId, EmailId emailId); + Future deleteSendingEmail(AccountId accountId, UserName userName, String sendingId); - Future updateEmailDrafts(AccountId accountId, Email newEmail, EmailId oldEmailId); + Future deleteMultipleSendingEmail(AccountId accountId, UserName userName, List sendingIds); - Future> deleteMultipleEmailsPermanently(AccountId accountId, List emailIds); + Future> updateMultipleSendingEmail(AccountId accountId, UserName userName, List newSendingEmails); - Future deleteEmailPermanently(AccountId accountId, EmailId emailId); + Future getStoredSendingEmail(AccountId accountId, UserName userName, String sendingId); } \ No newline at end of file diff --git a/lib/features/email/data/datasource/html_datasource.dart b/lib/features/email/data/datasource/html_datasource.dart index bcf7b67922..36f7d8975b 100644 --- a/lib/features/email/data/datasource/html_datasource.dart +++ b/lib/features/email/data/datasource/html_datasource.dart @@ -1,11 +1,16 @@ +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:model/model.dart'; abstract class HtmlDataSource { Future transformEmailContent( EmailContent emailContent, - Map mapUrlDownloadCID + Map mapCidImageDownloadUrl, + TransformConfiguration transformConfiguration ); - Future addTooltipWhenHoverOnLink(EmailContent emailContent); + Future transformHtmlEmailContent( + String htmlContent, + TransformConfiguration configuration + ); } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/calendar_event_datasource_impl.dart b/lib/features/email/data/datasource_impl/calendar_event_datasource_impl.dart new file mode 100644 index 0000000000..42c162ece6 --- /dev/null +++ b/lib/features/email/data/datasource_impl/calendar_event_datasource_impl.dart @@ -0,0 +1,28 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/calendar_event_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/network/calendar_event_api.dart'; +import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; + +class CalendarEventDataSourceImpl extends CalendarEventDataSource { + + final CalendarEventAPI _calendarEventAPI; + final ExceptionThrower _exceptionThrower; + + CalendarEventDataSourceImpl(this._calendarEventAPI, this._exceptionThrower); + + @override + Future>> parse(AccountId accountId, Set blobIds) { + return Future.sync(() async { + return await _calendarEventAPI.parse(accountId, blobIds); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future> getListEventAction(String emailContents) { + throw UnimplementedError(); + } +} \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/email_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_datasource_impl.dart index 0339c4770a..de40eacccf 100644 --- a/lib/features/email/data/datasource_impl/email_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/email_datasource_impl.dart @@ -7,13 +7,18 @@ import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; class EmailDataSourceImpl extends EmailDataSource { @@ -24,30 +29,34 @@ class EmailDataSourceImpl extends EmailDataSource { EmailDataSourceImpl(this.emailAPI, this._exceptionThrower); @override - Future getEmailContent(AccountId accountId, EmailId emailId) { + Future getEmailContent(Session session, AccountId accountId, EmailId emailId) { return Future.sync(() async { - return await emailAPI.getEmailContent(accountId, emailId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await emailAPI.getEmailContent(session, accountId, emailId); + }).catchError(_exceptionThrower.throwException); } @override - Future sendEmail(AccountId accountId, EmailRequest emailRequest, {CreateNewMailboxRequest? mailboxRequest}) { + Future sendEmail( + Session session, + AccountId accountId, + EmailRequest emailRequest, + {CreateNewMailboxRequest? mailboxRequest} + ) { return Future.sync(() async { - return await emailAPI.sendEmail(accountId, emailRequest, mailboxRequest: mailboxRequest); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await emailAPI.sendEmail(session, accountId, emailRequest, mailboxRequest: mailboxRequest); + }).catchError(_exceptionThrower.throwException); } @override - Future> markAsRead(AccountId accountId, List emails, ReadActions readActions) { + Future> markAsRead( + Session session, + AccountId accountId, + List emails, + ReadActions readActions + ) { return Future.sync(() async { - return await emailAPI.markAsRead(accountId, emails, readActions); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await emailAPI.markAsRead(session, accountId, emails, readActions); + }).catchError(_exceptionThrower.throwException); } @override @@ -59,9 +68,7 @@ class EmailDataSourceImpl extends EmailDataSource { ) { return Future.sync(() async { return await emailAPI.downloadAttachments(attachments, accountId, baseDownloadUrl, accountRequest); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override @@ -74,54 +81,42 @@ class EmailDataSourceImpl extends EmailDataSource { ) { return Future.sync(() async { return await emailAPI.exportAttachment(attachment, accountId, baseDownloadUrl, accountRequest, cancelToken); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future> moveToMailbox(AccountId accountId, MoveToMailboxRequest moveRequest) { + Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { return Future.sync(() async { - return await emailAPI.moveToMailbox(accountId, moveRequest); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await emailAPI.moveToMailbox(session, accountId, moveRequest); + }).catchError(_exceptionThrower.throwException); } @override - Future> markAsStar(AccountId accountId, List emails, MarkStarAction markStarAction) { + Future> markAsStar(Session session, AccountId accountId, List emails, MarkStarAction markStarAction) { return Future.sync(() async { - return await emailAPI.markAsStar(accountId, emails, markStarAction); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await emailAPI.markAsStar(session, accountId, emails, markStarAction); + }).catchError(_exceptionThrower.throwException); } @override - Future saveEmailAsDrafts(AccountId accountId, Email email) { + Future saveEmailAsDrafts(Session session, AccountId accountId, Email email) { return Future.sync(() async { - return await emailAPI.saveEmailAsDrafts(accountId, email); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await emailAPI.saveEmailAsDrafts(session, accountId, email); + }).catchError(_exceptionThrower.throwException); } @override - Future removeEmailDrafts(AccountId accountId, EmailId emailId) { + Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId) { return Future.sync(() async { - return await emailAPI.removeEmailDrafts(accountId, emailId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await emailAPI.removeEmailDrafts(session, accountId, emailId); + }).catchError(_exceptionThrower.throwException); } @override - Future updateEmailDrafts(AccountId accountId, Email newEmail, EmailId oldEmailId) { + Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId) { return Future.sync(() async { - return await emailAPI.updateEmailDrafts(accountId, newEmail, oldEmailId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await emailAPI.updateEmailDrafts(session, accountId, newEmail, oldEmailId); + }).catchError(_exceptionThrower.throwException); } @override @@ -141,26 +136,92 @@ class EmailDataSourceImpl extends EmailDataSource { baseDownloadUrl, accountRequest, onReceiveController); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds) { + return Future.sync(() async { + return await emailAPI.deleteMultipleEmailsPermanently(session, accountId, emailIds); + }).catchError(_exceptionThrower.throwException); } @override - Future> deleteMultipleEmailsPermanently(AccountId accountId, List emailIds) { + Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId) { return Future.sync(() async { - return await emailAPI.deleteMultipleEmailsPermanently(accountId, emailIds); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await emailAPI.deleteEmailPermanently(session, accountId, emailId); + }).catchError(_exceptionThrower.throwException); } @override - Future deleteEmailPermanently(AccountId accountId, EmailId emailId) { + Future storeDetailedNewEmail(Session session, AccountId accountId, DetailedEmail detailedEmail) { + throw UnimplementedError(); + } + + @override + Future> getListDetailedEmailById(Session session, AccountId accountId, Set emailIds, {Set? sort}) { return Future.sync(() async { - return await emailAPI.deleteEmailPermanently(accountId, emailId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await emailAPI.getListDetailedEmailById(session, accountId, emailIds, sort: sort); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future storeEmail(Session session, AccountId accountId, Email email) { + throw UnimplementedError(); + } + + @override + Future storeOpenedEmail(Session session, AccountId accountId, DetailedEmail detailedEmail) { + throw UnimplementedError(); + } + + @override + Future getStoredOpenedEmail(Session session, AccountId accountId, EmailId emailId) { + throw UnimplementedError(); + } + + @override + Future getStoredNewEmail(Session session, AccountId accountId, EmailId emailId) { + throw UnimplementedError(); + } + + @override + Future getStoredEmail(Session session, AccountId accountId, EmailId emailId) { + throw UnimplementedError(); + } + + @override + Future storeSendingEmail(AccountId accountId, UserName userName, SendingEmail sendingEmail) { + throw UnimplementedError(); + } + + @override + Future> getAllSendingEmails(AccountId accountId, UserName userName) { + throw UnimplementedError(); + } + + @override + Future deleteSendingEmail(AccountId accountId, UserName userName, String sendingId) { + throw UnimplementedError(); + } + + @override + Future updateSendingEmail(AccountId accountId, UserName userName, SendingEmail newSendingEmail) { + throw UnimplementedError(); + } + + @override + Future> updateMultipleSendingEmail(AccountId accountId, UserName userName, List newSendingEmails) { + throw UnimplementedError(); + } + + @override + Future> deleteMultipleSendingEmail(AccountId accountId, UserName userName, List sendingIds) { + throw UnimplementedError(); + } + + @override + Future getStoredSendingEmail(AccountId accountId, UserName userName, String sendingId) { + throw UnimplementedError(); } } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart new file mode 100644 index 0000000000..2b10b9b434 --- /dev/null +++ b/lib/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart @@ -0,0 +1,290 @@ + +import 'dart:async'; + +import 'dart:typed_data'; + +import 'package:core/data/network/download/downloaded_response.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/file_utils.dart'; +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/account/account_request.dart'; +import 'package:model/download/download_task_id.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/email/mark_star_action.dart'; +import 'package:model/email/read_actions.dart'; +import 'package:model/extensions/email_id_extensions.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; +import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/detailed_email_extension.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/detailed_email_hive_cache_extension.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; +import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/features/offline_mode/extensions/list_sending_email_hive_cache_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/extensions/sending_email_hive_cache_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/hive_worker/hive_task.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/detailed_email_hive_cache.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/extensions/list_sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/email_cache_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/email_extension.dart'; +import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; + +class EmailHiveCacheDataSourceImpl extends EmailDataSource { + + final NewEmailCacheManager _newEmailCacheManager; + final OpenedEmailCacheManager _openedEmailCacheManager; + final NewEmailCacheWorkerQueue _newEmailCacheWorkerQueue; + final OpenedEmailCacheWorkerQueue _openedEmailCacheWorkerQueue; + final EmailCacheManager _emailCacheManager; + final SendingEmailCacheManager _sendingEmailCacheManager; + final FileUtils _fileUtils; + final ExceptionThrower _exceptionThrower; + + EmailHiveCacheDataSourceImpl( + this._newEmailCacheManager, + this._openedEmailCacheManager, + this._newEmailCacheWorkerQueue, + this._openedEmailCacheWorkerQueue, + this._emailCacheManager, + this._sendingEmailCacheManager, + this._fileUtils, + this._exceptionThrower + ); + + @override + Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId) { + throw UnimplementedError(); + } + + @override + Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds) { + throw UnimplementedError(); + } + + @override + Future downloadAttachmentForWeb(DownloadTaskId taskId, Attachment attachment, AccountId accountId, String baseDownloadUrl, AccountRequest accountRequest, StreamController> onReceiveController) { + throw UnimplementedError(); + } + + @override + Future> downloadAttachments(List attachments, AccountId accountId, String baseDownloadUrl, AccountRequest accountRequest) { + throw UnimplementedError(); + } + + @override + Future exportAttachment(Attachment attachment, AccountId accountId, String baseDownloadUrl, AccountRequest accountRequest, CancelToken cancelToken) { + throw UnimplementedError(); + } + + @override + Future getEmailContent(Session session, AccountId accountId, EmailId emailId) { + throw UnimplementedError(); + } + + @override + Future> markAsRead(Session session, AccountId accountId, List emails, ReadActions readActions) { + throw UnimplementedError(); + } + + @override + Future> markAsStar(Session session, AccountId accountId, List emails, MarkStarAction markStarAction) { + throw UnimplementedError(); + } + + @override + Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { + throw UnimplementedError(); + } + + @override + Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId) { + throw UnimplementedError(); + } + + @override + Future saveEmailAsDrafts(Session session, AccountId accountId, Email email) { + throw UnimplementedError(); + } + + @override + Future sendEmail(Session session, AccountId accountId, EmailRequest emailRequest, {CreateNewMailboxRequest? mailboxRequest}) { + throw UnimplementedError(); + } + + @override + Future storeDetailedNewEmail(Session session, AccountId accountId, DetailedEmail detailedEmail) { + return Future.sync(() async { + final task = HiveTask( + id: detailedEmail.emailId.asString, + runnable: () async { + final fileSaved = await _fileUtils.saveToFile( + nameFile: detailedEmail.emailId.asString, + content: detailedEmail.htmlEmailContent ?? '', + folderPath: detailedEmail.newEmailFolderPath + ); + + final detailedEmailSaved = detailedEmail.fromEmailContentPath(fileSaved.path); + final detailedEmailCacheSaved = detailedEmailSaved.toHiveCache(); + + final detailedEmailCache = await _newEmailCacheManager.storeDetailedNewEmail( + accountId, + session.username, + detailedEmailCacheSaved); + + return detailedEmailCache; + }); + return _newEmailCacheWorkerQueue.addTask(task); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId) { + throw UnimplementedError(); + } + + @override + Future> getListDetailedEmailById(Session session, AccountId accountId, Set emailIds, {Set? sort}) { + throw UnimplementedError(); + } + + @override + Future storeEmail(Session session, AccountId accountId, Email email) { + return Future.sync(() async { + return await _emailCacheManager.storeEmail(accountId, session.username, email.toEmailCache()); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future storeOpenedEmail(Session session, AccountId accountId, DetailedEmail detailedEmail) { + return Future.sync(() async { + final task = HiveTask( + id: detailedEmail.emailId.asString, + runnable: () async { + final fileSaved = await _fileUtils.saveToFile( + nameFile: detailedEmail.emailId.asString, + content: detailedEmail.htmlEmailContent ?? '', + folderPath: detailedEmail.openedEmailFolderPath + ); + + final detailedEmailSaved = detailedEmail.fromEmailContentPath(fileSaved.path); + + final detailedEmailCache = await _openedEmailCacheManager.storeOpenedEmail( + accountId, + session.username, + detailedEmailSaved.toHiveCache()); + + return detailedEmailCache; + }); + return _openedEmailCacheWorkerQueue.addTask(task); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future getStoredOpenedEmail(Session session, AccountId accountId, EmailId emailId) { + return Future.sync(() async { + final listResult = await Future.wait([ + _openedEmailCacheManager.getStoredOpenedEmail(accountId, session.username, emailId), + _fileUtils.getContentFromFile(nameFile: emailId.asString, folderPath: CachingConstants.openedEmailContentFolderName) + ], eagerError: true); + + final detailedEmailCache = listResult[0] as DetailedEmailHiveCache; + final emailContent = listResult[1] as String; + log('EmailHiveCacheDataSourceImpl::getStoredOpenedEmail():detailedEmailCache: ${detailedEmailCache.emailId} | emailContent: $emailContent'); + return detailedEmailCache.toDetailedEmailWithContent(emailContent); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future getStoredNewEmail(Session session, AccountId accountId, EmailId emailId) { + return Future.sync(() async { + final listResult = await Future.wait([ + _newEmailCacheManager.getStoredNewEmail(accountId, session.username, emailId), + _fileUtils.getContentFromFile(nameFile: emailId.asString, folderPath: CachingConstants.newEmailsContentFolderName) + ], eagerError: true); + + final detailedEmailCache = listResult[0] as DetailedEmailHiveCache; + final emailContent = listResult[1] as String; + log('EmailHiveCacheDataSourceImpl::getStoredNewEmail():detailedEmailCache: ${detailedEmailCache.emailId} | emailContent: $emailContent'); + return detailedEmailCache.toDetailedEmailWithContent(emailContent); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future getStoredEmail(Session session, AccountId accountId, EmailId emailId) { + return Future.sync(() async { + final email = await _emailCacheManager.getStoredEmail(accountId, session.username, emailId); + return email.toEmail(); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future storeSendingEmail(AccountId accountId, UserName userName, SendingEmail sendingEmail) { + return Future.sync(() async { + final sendingEmailsCache = await _sendingEmailCacheManager.storeSendingEmail(accountId, userName, sendingEmail.toHiveCache()); + return sendingEmailsCache.toSendingEmail(); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future> getAllSendingEmails(AccountId accountId, UserName userName) { + return Future.sync(() async { + final sendingEmailsCache = await _sendingEmailCacheManager.getAllSendingEmailsByTupleKey(accountId, userName); + return sendingEmailsCache.toSendingEmails(); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future deleteSendingEmail(AccountId accountId, UserName userName, String sendingId) { + return Future.sync(() async { + return await _sendingEmailCacheManager.deleteSendingEmail(accountId, userName, sendingId); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future updateSendingEmail(AccountId accountId, UserName userName, SendingEmail newSendingEmail) { + return Future.sync(() async { + final sendingEmailsCache = await _sendingEmailCacheManager.updateSendingEmail(accountId, userName, newSendingEmail.toHiveCache()); + return sendingEmailsCache.toSendingEmail(); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future> updateMultipleSendingEmail(AccountId accountId, UserName userName, List newSendingEmails) { + return Future.sync(() async { + final listSendingEmailsCache = await _sendingEmailCacheManager.updateMultipleSendingEmail(accountId, userName, newSendingEmails.toHiveCache()); + return listSendingEmailsCache.toSendingEmails(); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future deleteMultipleSendingEmail(AccountId accountId, UserName userName, List sendingIds) { + return Future.sync(() async { + return await _sendingEmailCacheManager.deleteMultipleSendingEmail(accountId, userName, sendingIds); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future getStoredSendingEmail(AccountId accountId, UserName userName, String sendingId) { + return Future.sync(() async { + final sendingEmailCache = await _sendingEmailCacheManager.getStoredSendingEmail(accountId, userName, sendingId); + return sendingEmailCache.toSendingEmail(); + }).catchError(_exceptionThrower.throwException); + } +} \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/html_datasource_impl.dart b/lib/features/email/data/datasource_impl/html_datasource_impl.dart index 11b557cbe9..7ce83177e6 100644 --- a/lib/features/email/data/datasource_impl/html_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/html_datasource_impl.dart @@ -1,5 +1,5 @@ -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:model/email/email_content.dart'; import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; @@ -7,29 +7,32 @@ import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; class HtmlDataSourceImpl extends HtmlDataSource { final HtmlAnalyzer _htmlAnalyzer; - final DioClient _dioClient; final ExceptionThrower _exceptionThrower; - HtmlDataSourceImpl(this._htmlAnalyzer, this._dioClient, this._exceptionThrower); + HtmlDataSourceImpl(this._htmlAnalyzer, this._exceptionThrower); @override Future transformEmailContent( - EmailContent emailContent, - Map? mapUrlDownloadCID + EmailContent emailContent, + Map mapCidImageDownloadUrl, + TransformConfiguration transformConfiguration ) { return Future.sync(() async { - return await _htmlAnalyzer.transformEmailContent(emailContent, mapUrlDownloadCID, _dioClient); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await _htmlAnalyzer.transformEmailContent( + emailContent, + mapCidImageDownloadUrl, + transformConfiguration + ); + }).catchError(_exceptionThrower.throwException); } @override - Future addTooltipWhenHoverOnLink(EmailContent emailContent) { + Future transformHtmlEmailContent(String htmlContent, TransformConfiguration configuration) { return Future.sync(() async { - return await _htmlAnalyzer.addTooltipWhenHoverOnLink(emailContent); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await _htmlAnalyzer.transformHtmlEmailContent( + htmlContent, + configuration + ); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/local_calendar_event_datasource_impl.dart b/lib/features/email/data/datasource_impl/local_calendar_event_datasource_impl.dart new file mode 100644 index 0000000000..115af75388 --- /dev/null +++ b/lib/features/email/data/datasource_impl/local_calendar_event_datasource_impl.dart @@ -0,0 +1,28 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/calendar_event_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; +import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; + +class LocalCalendarEventDataSourceImpl extends CalendarEventDataSource { + + final HtmlAnalyzer _htmlAnalyzer; + final ExceptionThrower _exceptionThrower; + + LocalCalendarEventDataSourceImpl(this._htmlAnalyzer, this._exceptionThrower); + + @override + Future>> parse(AccountId accountId, Set blobIds) { + throw UnimplementedError(); + } + + @override + Future> getListEventAction(String emailContents) { + return Future.sync(() async { + return await _htmlAnalyzer.getListEventAction(emailContents); + }).catchError(_exceptionThrower.throwException); + } +} \ No newline at end of file diff --git a/lib/features/email/data/datasource_impl/mdn_datasource_impl.dart b/lib/features/email/data/datasource_impl/mdn_datasource_impl.dart index efe664086b..fc23d9d4a4 100644 --- a/lib/features/email/data/datasource_impl/mdn_datasource_impl.dart +++ b/lib/features/email/data/datasource_impl/mdn_datasource_impl.dart @@ -18,8 +18,6 @@ class MdnDataSourceImpl extends MdnDataSource { Future sendReceiptToSender(AccountId accountId, SendReceiptToSenderRequest request) { return Future.sync(() async { return await _mdnAPI.sendReceiptToSender(accountId, request); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/email/data/local/html_analyzer.dart b/lib/features/email/data/local/html_analyzer.dart index 9517a38e35..ca7550dc0e 100644 --- a/lib/features/email/data/local/html_analyzer.dart +++ b/lib/features/email/data/local/html_analyzer.dart @@ -1,44 +1,102 @@ - -import 'package:core/core.dart'; -import 'package:core/presentation/utils/html_transformer/text/convert_url_string_to_html_links_transformers.dart'; -import 'package:model/model.dart'; +import 'package:collection/collection.dart'; +import 'package:core/presentation/utils/html_transformer/html_transform.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:html/parser.dart'; +import 'package:model/email/email_content.dart'; +import 'package:model/email/email_content_type.dart'; +import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; class HtmlAnalyzer { + final HtmlTransform _htmlTransform; + + HtmlAnalyzer(this._htmlTransform); + Future transformEmailContent( - EmailContent emailContent, - Map? mapUrlDownloadCID, - DioClient dioClient + EmailContent emailContent, + Map mapCidImageDownloadUrl, + TransformConfiguration transformConfiguration ) async { switch(emailContent.type) { case EmailContentType.textHtml: - final htmlTransform = HtmlTransform( - emailContent.content, - dioClient: dioClient, - mapUrlDownloadCID: mapUrlDownloadCID); - final htmlContent = await htmlTransform.transformToHtml(); + final htmlContent = await _htmlTransform.transformToHtml( + htmlContent: emailContent.content, + mapCidImageDownloadUrl: mapCidImageDownloadUrl, + transformConfiguration: transformConfiguration + ); + return EmailContent(emailContent.type, htmlContent); case EmailContentType.textPlain: - final htmlTransform = HtmlTransform(emailContent.content); - final message = htmlTransform.transformToTextPlain( - transformConfiguration: TransformConfiguration.create( - customTextTransformers: [const ConvertUrlStringToHtmlLinksTransformers()])); + final message = _htmlTransform.transformToTextPlain( + content: emailContent.content, + transformConfiguration: transformConfiguration + ); return EmailContent(emailContent.type, message); default: return emailContent; } } - Future addTooltipWhenHoverOnLink(EmailContent emailContent) async { - switch(emailContent.type) { - case EmailContentType.textHtml: - final htmlTransform = HtmlTransform(emailContent.content); - final htmlContent = await htmlTransform.transformToHtml( - transformConfiguration: TransformConfiguration.create( - customDomTransformers: [const AddTooltipLinkTransformer()])); - return EmailContent(emailContent.type, htmlContent); - default: - return emailContent; + Future> getListEventAction(String emailContents) async { + try { + final document = parse(emailContents); + + final openPaasLinkElements = document.querySelectorAll('a.part-button'); + if (openPaasLinkElements.isNotEmpty) { + final listEventAction = openPaasLinkElements + .mapIndexed((index, element) { + final hrefLink = element.attributes['href'] ?? ''; + if (hrefLink.isNotEmpty) { + if (index == 0) { + return EventAction(EventActionType.yes, hrefLink); + } else if (index == 1) { + return EventAction(EventActionType.maybe, hrefLink); + } else if (index == 2) { + return EventAction(EventActionType.no, hrefLink); + } + } + return null; + }) + .whereNotNull() + .toList(); + log('HtmlAnalyzer::getListEventAction:OPEN_PAAS::listEventAction: $listEventAction'); + return listEventAction; + } else { + final googleLinkElements = document.querySelectorAll('a.grey-button-text'); + final listEventAction = googleLinkElements + .mapIndexed((index, element) { + final hrefLink = element.attributes['href'] ?? ''; + if (hrefLink.isNotEmpty) { + if (index == 0) { + return EventAction(EventActionType.yes, hrefLink); + } else if (index == 1) { + return EventAction(EventActionType.no, hrefLink); + } else if (index == 2) { + return EventAction(EventActionType.maybe, hrefLink); + } + } + return null; + }) + .whereNotNull() + .toList(); + log('HtmlAnalyzer::getListEventAction:GOOGLE::listEventAction: $listEventAction'); + return listEventAction; + } + } catch(e) { + logError('HtmlAnalyzer::getListEventAction:Exception: $e'); + return []; } } + + Future transformHtmlEmailContent( + String htmlContent, + TransformConfiguration configuration + ) async { + final htmlContentTransformed = await _htmlTransform.transformToHtml( + htmlContent: htmlContent, + transformConfiguration: configuration + ); + return htmlContentTransformed; + } } \ No newline at end of file diff --git a/lib/features/email/data/network/calendar_event_api.dart b/lib/features/email/data/network/calendar_event_api.dart new file mode 100644 index 0000000000..2f50c1e481 --- /dev/null +++ b/lib/features/email/data/network/calendar_event_api.dart @@ -0,0 +1,41 @@ +import 'dart:async'; + +import 'package:jmap_dart_client/http/http_client.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/jmap_request.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/parse/calendar_event_parse_method.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/parse/calendar_event_parse_response.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/calendar_event_exceptions.dart'; + +class CalendarEventAPI { + + final HttpClient _httpClient; + + CalendarEventAPI(this._httpClient); + + Future>> parse(AccountId accountId, Set blobIds) async { + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + final calendarEventParseMethod = CalendarEventParseMethod(accountId, blobIds); + final calendarEventParseInvocation = requestBuilder.invocation(calendarEventParseMethod); + final response = await (requestBuilder + ..usings(calendarEventParseMethod.requiredCapabilities)) + .build() + .execute(); + + final calendarEventParseResponse = response.parse( + calendarEventParseInvocation.methodCallId, + CalendarEventParseResponse.deserialize); + + if (calendarEventParseResponse?.parsed?.isNotEmpty == true) { + return calendarEventParseResponse!.parsed!; + } else if (calendarEventParseResponse?.notParsable?.isNotEmpty == true) { + throw NotParsableCalendarEventException(); + } else if (calendarEventParseResponse?.notFound?.isNotEmpty == true) { + throw NotFoundCalendarEventException(); + } else { + throw NotParsableCalendarEventException(); + } + } +} \ No newline at end of file diff --git a/lib/features/email/data/network/email_api.dart b/lib/features/email/data/network/email_api.dart index 297572a167..c4e20c8e02 100644 --- a/lib/features/email/data/network/email_api.dart +++ b/lib/features/email/data/network/email_api.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; @@ -12,11 +13,15 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/capability/core_capability.dart'; import 'package:jmap_dart_client/jmap/core/error/set_error.dart'; +import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/method/response/set_response.dart'; import 'package:jmap_dart_client/jmap/core/patch_object.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/reference_id.dart'; import 'package:jmap_dart_client/jmap/core/reference_prefix.dart'; +import 'package:jmap_dart_client/jmap/core/request/request_invocation.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/jmap_request.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/get/get_email_method.dart'; @@ -36,11 +41,11 @@ import 'package:model/account/account_request.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/download/download_task_id.dart'; import 'package:model/email/attachment.dart'; -import 'package:model/email/email_property.dart'; import 'package:model/email/mark_star_action.dart'; import 'package:model/email/read_actions.dart'; import 'package:model/extensions/email_extension.dart'; import 'package:model/extensions/keyword_identifier_extension.dart'; +import 'package:model/extensions/list_email_extension.dart'; import 'package:model/extensions/list_email_id_extension.dart'; import 'package:model/extensions/mailbox_id_extension.dart'; import 'package:model/extensions/session_extension.dart'; @@ -49,55 +54,58 @@ import 'package:path_provider/path_provider.dart'; import 'package:tmail_ui_user/features/base/mixin/handle_error_mixin.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/set_email_method_exception.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_exceptions.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachment_for_web_state.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; +import 'package:uuid/uuid.dart'; class EmailAPI with HandleSetErrorMixin { final HttpClient _httpClient; final DownloadManager _downloadManager; final DioClient _dioClient; + final Uuid _uuid; - EmailAPI(this._httpClient, this._downloadManager, this._dioClient); + EmailAPI(this._httpClient, this._downloadManager, this._dioClient, this._uuid); - Future getEmailContent(AccountId accountId, EmailId emailId) async { + Future getEmailContent(Session session, AccountId accountId, EmailId emailId) async { final processingInvocation = ProcessingInvocation(); final jmapRequestBuilder = JmapRequestBuilder(_httpClient, processingInvocation); final getEmailMethod = GetEmailMethod(accountId) ..addIds({emailId.id}) - ..addProperties(Properties({ - EmailProperty.bodyValues, - EmailProperty.htmlBody, - EmailProperty.attachments, - EmailProperty.headers, - EmailProperty.keywords, - EmailProperty.mailboxIds, - })) + ..addProperties(ThreadConstants.propertiesGetEmailContent) ..addFetchHTMLBodyValues(true); final getEmailInvocation = jmapRequestBuilder.invocation(getEmailMethod); + final capabilities = getEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final result = await (jmapRequestBuilder - ..usings(getEmailMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); final resultList = result.parse( - getEmailInvocation.methodCallId, GetEmailResponse.deserialize); + getEmailInvocation.methodCallId, + GetEmailResponse.deserialize + ); - return Future.sync(() async { + if (resultList?.list.isNotEmpty == true) { return resultList!.list.first; - }).catchError((error) { - throw error; - }); + } else { + throw NotFoundEmailException(); + } } Future sendEmail( + Session session, AccountId accountId, EmailRequest emailRequest, {CreateNewMailboxRequest? mailboxRequest} @@ -111,7 +119,12 @@ class EmailAPI with HandleSetErrorMixin { final setMailboxMethod = SetMailboxMethod(accountId) ..addCreate( mailboxRequest.creationId, - Mailbox(name: mailboxRequest.newName, parentId: mailboxRequest.parentId)); + Mailbox( + name: mailboxRequest.newName, + parentId: mailboxRequest.parentId, + isSubscribed: IsSubscribed(mailboxRequest.isSubscribed) + ) + ); requestBuilder.invocation(setMailboxMethod); @@ -124,38 +137,57 @@ class EmailAPI with HandleSetErrorMixin { emailNeedsToBeCreated = emailRequest.email; } + final idCreateMethod = Id(_uuid.v1()); final setEmailMethod = SetEmailMethod(accountId) - ..addCreate(emailNeedsToBeCreated.id.id, emailNeedsToBeCreated); - - if (emailRequest.emailIdDestroyed != null) { - setEmailMethod.addDestroy({emailRequest.emailIdDestroyed!.id}); - } - final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + ..addCreate(idCreateMethod, emailNeedsToBeCreated); + + final submissionCreateId = Id(_uuid.v1()); + final mailFrom = Address(emailNeedsToBeCreated.from?.first.email ?? ''); + final recipientsList = emailNeedsToBeCreated.getRecipientEmailAddressList() + .map((emailAddress) => Address(emailAddress)) + .toSet(); + final emailSubmissionId = EmailSubmissionId(ReferenceId(ReferencePrefix.defaultPrefix, submissionCreateId)); + Map mapEmailSubmissionUpdated = { + emailSubmissionId: PatchObject({ + emailRequest.sentMailboxId!.generatePath() : true, + outboxMailboxId!.generatePath() : null, + KeyWordIdentifier.emailSeen.generatePath(): true, + KeyWordIdentifier.emailDraft.generatePath(): null + }) + }; + final emailSubmission = EmailSubmission( + identityId: emailRequest.identityId?.id, + emailId: EmailId(ReferenceId(ReferencePrefix.defaultPrefix, idCreateMethod)), + envelope: Envelope(mailFrom, recipientsList)); final setEmailSubmissionMethod = SetEmailSubmissionMethod(accountId) - ..addCreate( - emailRequest.submissionCreateId, - EmailSubmission( - identityId: emailRequest.identity?.id?.id, - emailId: EmailId(ReferenceId(ReferencePrefix.defaultPrefix, emailNeedsToBeCreated.id.id)), - envelope: Envelope( - Address(emailNeedsToBeCreated.from?.first.email ?? ''), - emailNeedsToBeCreated.getRecipientEmailAddressList().map((emailAddress) => Address(emailAddress)).toSet() - ) - )) - ..addOnSuccessUpdateEmail({ - EmailSubmissionId(ReferenceId(ReferencePrefix.defaultPrefix, emailRequest.submissionCreateId)): PatchObject({ - emailRequest.sentMailboxId!.generatePath() : true, - outboxMailboxId!.generatePath() : null, - KeyWordIdentifier.emailSeen.generatePath(): true, - KeyWordIdentifier.emailDraft.generatePath(): null - }) - }); + ..addCreate(submissionCreateId, emailSubmission) + ..addOnSuccessUpdateEmail(mapEmailSubmissionUpdated); + final setEmailInvocation = requestBuilder.invocation(setEmailMethod); final setEmailSubmissionInvocation = requestBuilder.invocation(setEmailSubmissionMethod); + SetEmailMethod? markAsAnsweredOrForwardedSetMethod; + RequestInvocation? markAsAnsweredOrForwardedInvocation; + SetEmailResponse? markAsAnsweredOrForwardedSetResponse; + + if (emailRequest.isEmailAnswered) { + markAsAnsweredOrForwardedSetMethod = SetEmailMethod(accountId) + ..addUpdates([emailRequest.emailIdAnsweredOrForwarded!].generateMapUpdateObjectMarkAsAnswered()); + + markAsAnsweredOrForwardedInvocation = requestBuilder.invocation(markAsAnsweredOrForwardedSetMethod); + } else if (emailRequest.isEmailForwarded) { + markAsAnsweredOrForwardedSetMethod = SetEmailMethod(accountId) + ..addUpdates([emailRequest.emailIdAnsweredOrForwarded!].generateMapUpdateObjectMarkAsForwarded()); + + markAsAnsweredOrForwardedInvocation = requestBuilder.invocation(markAsAnsweredOrForwardedSetMethod); + } + + final capabilities = setEmailSubmissionMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setEmailSubmissionMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -168,11 +200,18 @@ class EmailAPI with HandleSetErrorMixin { SetEmailSubmissionResponse.deserialize, methodName: setEmailInvocation.methodName); - final emailCreated = setEmailResponse?.created?[emailNeedsToBeCreated.id.id]; - final listEntriesErrors = _handleSetEmailResponse( - response: setEmailResponse, - submissionResponse: setEmailSubmissionResponse - ); + if (markAsAnsweredOrForwardedInvocation != null) { + markAsAnsweredOrForwardedSetResponse = response.parse( + markAsAnsweredOrForwardedInvocation.methodCallId, + SetEmailResponse.deserialize); + } + + final emailCreated = setEmailResponse?.created?[idCreateMethod]; + final listEntriesErrors = _handleSetEmailResponse([ + setEmailResponse, + setEmailSubmissionResponse, + markAsAnsweredOrForwardedSetResponse + ]); final mapErrors = Map.fromEntries(listEntriesErrors); if (emailCreated != null && mapErrors.isEmpty) { @@ -182,12 +221,14 @@ class EmailAPI with HandleSetErrorMixin { } } - List> _handleSetEmailResponse({ - SetEmailResponse? response, - SetEmailSubmissionResponse? submissionResponse - }) { + List> _handleSetEmailResponse(List listSetResponse) { + final listSetResponseNotNull = listSetResponse.whereNotNull().toList(); + if (listSetResponseNotNull.isEmpty) { + return []; + } + final List> remainedErrors = []; - if (response != null) { + for (var response in listSetResponseNotNull) { handleSetErrors( notDestroyedError: response.notDestroyed, notUpdatedError: response.notUpdated, @@ -198,28 +239,20 @@ class EmailAPI with HandleSetErrorMixin { } ); } - if (submissionResponse != null) { - handleSetErrors( - notDestroyedError: submissionResponse.notDestroyed, - notUpdatedError: submissionResponse.notUpdated, - notCreatedError: submissionResponse.notCreated, - unCatchErrorHandler: (setErrorEntry) { - remainedErrors.add(setErrorEntry); - return false; - } - ); - } return remainedErrors; } - Future> markAsRead(AccountId accountId, List emails, ReadActions readActions) async { - final emailIds = emails.map((email) => email.id).toList(); - + Future> markAsRead( + Session session, + AccountId accountId, + List emails, + ReadActions readActions + ) async { final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(emailIds.generateMapUpdateObjectMarkAsRead(readActions)); + ..addUpdates(emails.listEmailIds.generateMapUpdateObjectMarkAsRead(readActions)); final getEmailMethod = GetEmailMethod(accountId) - ..addIds(emailIds.toIds().toSet()) + ..addIds(emails.listEmailIds.toIds().toSet()) ..addProperties(Properties({'keywords'})); final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); @@ -228,8 +261,11 @@ class EmailAPI with HandleSetErrorMixin { final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setEmailMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -344,13 +380,17 @@ class EmailAPI with HandleSetErrorMixin { return bytesDownloaded; } - Future> moveToMailbox(AccountId accountId, MoveToMailboxRequest moveRequest) async { + Future> moveToMailbox( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest + ) async { requireCapability(moveRequest.session, accountId, [CapabilityIdentifier.jmapCore, CapabilityIdentifier.jmapMail]); final coreCapability = moveRequest.session.getCapabilityProperties( accountId, CapabilityIdentifier.jmapCore); - final maxMethodCount = coreCapability.maxCallsInRequest.value.toInt(); + final maxMethodCount = coreCapability.maxCallsInRequest?.value.toInt() ?? 0; var start = 0; var end = 0; @@ -374,7 +414,10 @@ class EmailAPI with HandleSetErrorMixin { ..addUpdates(currentItem.value.generateMapUpdateObjectMoveToMailbox(currentItem.key, moveRequest.destinationMailboxId)); }).map(requestBuilder.invocation).toList(); - final response = await (requestBuilder..usings({CapabilityIdentifier.jmapCore, CapabilityIdentifier.jmapMail})) + final capabilities = {CapabilityIdentifier.jmapCore, CapabilityIdentifier.jmapMail} + .toCapabilitiesSupportTeamMailboxes(session, accountId); + + final response = await (requestBuilder..usings(capabilities)) .build() .execute(); @@ -399,15 +442,17 @@ class EmailAPI with HandleSetErrorMixin { return listEmailIdRequest.where((emailId) => listUpdated.expand((e) => e).toList().contains(emailId.id)).toList(); } - - Future> markAsStar(AccountId accountId, List emails, MarkStarAction markStarAction) async { - final emailIds = emails.map((email) => email.id).toList(); - + Future> markAsStar( + Session session, + AccountId accountId, + List emails, + MarkStarAction markStarAction + ) async { final setEmailMethod = SetEmailMethod(accountId) - ..addUpdates(emailIds.generateMapUpdateObjectMarkAsStar(markStarAction)); + ..addUpdates(emails.listEmailIds.generateMapUpdateObjectMarkAsStar(markStarAction)); final getEmailMethod = GetEmailMethod(accountId) - ..addIds(emailIds.toIds().toSet()) + ..addIds(emails.listEmailIds.toIds().toSet()) ..addProperties(Properties({'keywords'})); final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); @@ -416,8 +461,11 @@ class EmailAPI with HandleSetErrorMixin { final getEmailInvocation = requestBuilder.invocation(getEmailMethod); + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setEmailMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -432,16 +480,20 @@ class EmailAPI with HandleSetErrorMixin { }); } - Future saveEmailAsDrafts(AccountId accountId, Email email) async { + Future saveEmailAsDrafts(Session session, AccountId accountId, Email email) async { + final idCreateMethod = Id(_uuid.v1()); final setEmailMethod = SetEmailMethod(accountId) - ..addCreate(email.id.id, email); + ..addCreate(idCreateMethod, email); final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setEmailMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -450,8 +502,8 @@ class EmailAPI with HandleSetErrorMixin { SetEmailResponse.deserialize ); - final emailCreated = setEmailResponse?.created?[email.id.id]; - final listEntriesErrors = _handleSetEmailResponse(response: setEmailResponse); + final emailCreated = setEmailResponse?.created?[idCreateMethod]; + final listEntriesErrors = _handleSetEmailResponse([setEmailResponse]); final mapErrors = Map.fromEntries(listEntriesErrors); if (emailCreated != null && mapErrors.isEmpty) { @@ -461,7 +513,7 @@ class EmailAPI with HandleSetErrorMixin { } } - Future removeEmailDrafts(AccountId accountId, EmailId emailId) async { + Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId) async { final setEmailMethod = SetEmailMethod(accountId) ..addDestroy({emailId.id}); @@ -469,8 +521,11 @@ class EmailAPI with HandleSetErrorMixin { final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setEmailMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -485,17 +540,26 @@ class EmailAPI with HandleSetErrorMixin { }); } - Future updateEmailDrafts(AccountId accountId, Email newEmail, EmailId oldEmailId) async { + Future updateEmailDrafts( + Session session, + AccountId accountId, + Email newEmail, + EmailId oldEmailId + ) async { + final idCreateMethod = Id(_uuid.v1()); final setEmailMethod = SetEmailMethod(accountId) - ..addCreate(newEmail.id.id, newEmail) + ..addCreate(idCreateMethod, newEmail) ..addDestroy({oldEmailId.id}); final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setEmailMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -504,27 +568,34 @@ class EmailAPI with HandleSetErrorMixin { SetEmailResponse.deserialize ); - final emailUpdated = setEmailResponse?.created?[newEmail.id.id]; - final isEmailDestroyedSuccess = setEmailResponse?.destroyed?.contains(oldEmailId.id) ?? false; - final listEntriesErrors = _handleSetEmailResponse(response: setEmailResponse); + final emailUpdated = setEmailResponse?.created?[idCreateMethod]; + final isEmailDeleted = setEmailResponse?.destroyed?.contains(oldEmailId.id); + final listEntriesErrors = _handleSetEmailResponse([setEmailResponse]); final mapErrors = Map.fromEntries(listEntriesErrors); - if (emailUpdated != null && isEmailDestroyedSuccess && mapErrors.isEmpty) { + if (emailUpdated != null && isEmailDeleted == true && mapErrors.isEmpty) { return emailUpdated; } else { throw SetEmailMethodException(mapErrors); } } - Future> deleteMultipleEmailsPermanently(AccountId accountId, List emailIds) async { + Future> deleteMultipleEmailsPermanently( + Session session, + AccountId accountId, + List emailIds + ) async { final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); final setEmailMethod = SetEmailMethod(accountId) ..addDestroy(emailIds.map((emailId) => emailId.id).toSet()); final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setEmailMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -541,15 +612,18 @@ class EmailAPI with HandleSetErrorMixin { return List.empty(); } - Future deleteEmailPermanently(AccountId accountId, EmailId emailId) async { + Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId) async { final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); final setEmailMethod = SetEmailMethod(accountId) ..addDestroy({emailId.id}); final setEmailInvocation = requestBuilder.invocation(setEmailMethod); + final capabilities = setEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setEmailMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -559,4 +633,43 @@ class EmailAPI with HandleSetErrorMixin { return setEmailResponse?.destroyed?.contains(emailId.id) == true; } + + Future> getListDetailedEmailById( + Session session, + AccountId accountId, + Set emailIds, + {Set? sort} + ) async { + final jmapRequestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); + + final getEmailMethod = GetEmailMethod(accountId) + ..addIds(emailIds.map((emailId) => emailId.id).toSet()) + ..addProperties(ThreadConstants.propertiesGetDetailedEmail) + ..addFetchHTMLBodyValues(true); + + final getEmailInvocation = jmapRequestBuilder.invocation(getEmailMethod); + + final capabilities = getEmailMethod.requiredCapabilities.toCapabilitiesSupportTeamMailboxes(session, accountId); + + final result = await (jmapRequestBuilder + ..usings(capabilities)) + .build() + .execute(); + + final resultList = result.parse( + getEmailInvocation.methodCallId, + GetEmailResponse.deserialize); + + if (sort != null && resultList != null) { + for (var comparator in sort) { + resultList.sortEmails(comparator); + } + } + + if (resultList?.list.isNotEmpty == true) { + return resultList!.list; + } else { + throw NotFoundEmailException(); + } + } } \ No newline at end of file diff --git a/lib/features/email/data/repository/calendar_event_repository_impl.dart b/lib/features/email/data/repository/calendar_event_repository_impl.dart new file mode 100644 index 0000000000..877b2dbfdf --- /dev/null +++ b/lib/features/email/data/repository/calendar_event_repository_impl.dart @@ -0,0 +1,25 @@ + +import 'package:core/data/model/source_type/data_source_type.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/calendar_event_datasource.dart'; +import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/calendar_event_repository.dart'; + +class CalendarEventRepositoryImpl extends CalendarEventRepository { + + final Map _calendarEventDataSource; + + CalendarEventRepositoryImpl(this._calendarEventDataSource); + + @override + Future>> parse(AccountId accountId, Set blobIds) { + return _calendarEventDataSource[DataSourceType.network]!.parse(accountId, blobIds); + } + + @override + Future> getListEventAction(String emailContents) { + return _calendarEventDataSource[DataSourceType.local]!.getListEventAction(emailContents); + } +} \ No newline at end of file diff --git a/lib/features/email/data/repository/email_repository_impl.dart b/lib/features/email/data/repository/email_repository_impl.dart index 5bb0d53cc0..a98c51b573 100644 --- a/lib/features/email/data/repository/email_repository_impl.dart +++ b/lib/features/email/data/repository/email_repository_impl.dart @@ -2,16 +2,28 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:core/core.dart'; +import 'package:core/data/model/source_type/data_source_type.dart'; +import 'package:core/data/network/download/downloaded_response.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:model/model.dart'; +import 'package:model/account/account_request.dart'; +import 'package:model/download/download_task_id.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/email/email_content.dart'; +import 'package:model/email/mark_star_action.dart'; +import 'package:model/email/read_actions.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; @@ -20,7 +32,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_r class EmailRepositoryImpl extends EmailRepository { - final EmailDataSource emailDataSource; + final Map emailDataSource; final HtmlDataSource _htmlDataSource; final StateDataSource _stateDataSource; @@ -31,18 +43,28 @@ class EmailRepositoryImpl extends EmailRepository { ); @override - Future getEmailContent(AccountId accountId, EmailId emailId) { - return emailDataSource.getEmailContent(accountId, emailId); + Future getEmailContent(Session session, AccountId accountId, EmailId emailId) { + return emailDataSource[DataSourceType.network]!.getEmailContent(session ,accountId, emailId); } @override - Future sendEmail(AccountId accountId, EmailRequest emailRequest, {CreateNewMailboxRequest? mailboxRequest}) { - return emailDataSource.sendEmail(accountId, emailRequest, mailboxRequest: mailboxRequest); + Future sendEmail( + Session session, + AccountId accountId, + EmailRequest emailRequest, + {CreateNewMailboxRequest? mailboxRequest} + ) { + return emailDataSource[DataSourceType.network]!.sendEmail(session, accountId, emailRequest, mailboxRequest: mailboxRequest); } @override - Future> markAsRead(AccountId accountId, List emails, ReadActions readActions) { - return emailDataSource.markAsRead(accountId, emails, readActions); + Future> markAsRead( + Session session, + AccountId accountId, + List emails, + ReadActions readActions + ) { + return emailDataSource[DataSourceType.network]!.markAsRead(session, accountId, emails, readActions); } @override @@ -52,7 +74,7 @@ class EmailRepositoryImpl extends EmailRepository { String baseDownloadUrl, AccountRequest accountRequest ) { - return emailDataSource.downloadAttachments(attachments, accountId, baseDownloadUrl, accountRequest); + return emailDataSource[DataSourceType.network]!.downloadAttachments(attachments, accountId, baseDownloadUrl, accountRequest); } @override @@ -63,7 +85,7 @@ class EmailRepositoryImpl extends EmailRepository { AccountRequest accountRequest, CancelToken cancelToken ) { - return emailDataSource.exportAttachment( + return emailDataSource[DataSourceType.network]!.exportAttachment( attachment, accountId, baseDownloadUrl, @@ -72,50 +94,50 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future> moveToMailbox(AccountId accountId, MoveToMailboxRequest moveRequest) { - return emailDataSource.moveToMailbox(accountId, moveRequest); + Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { + return emailDataSource[DataSourceType.network]!.moveToMailbox(session, accountId, moveRequest); } @override Future> markAsStar( - AccountId accountId, - List emails, - MarkStarAction markStarAction + Session session, + AccountId accountId, + List emails, + MarkStarAction markStarAction ) { - return emailDataSource.markAsStar(accountId, emails, markStarAction); + return emailDataSource[DataSourceType.network]!.markAsStar(session, accountId, emails, markStarAction); } @override Future> transformEmailContent( - List emailContents, - List attachmentInlines, - String? baseUrlDownload, - AccountId accountId - ) async { - final mapUrlDownloadCID = { - for (var attachment in attachmentInlines) - attachment.cid! : attachment.getDownloadUrl(baseUrlDownload!, accountId) - }; + List emailContents, + Map mapCidImageDownloadUrl, + TransformConfiguration transformConfiguration + ) async { return await Future.wait(emailContents .map((emailContent) async { - return await _htmlDataSource.transformEmailContent(emailContent, mapUrlDownloadCID); + return await _htmlDataSource.transformEmailContent( + emailContent, + mapCidImageDownloadUrl, + transformConfiguration, + ); }) .toList()); } @override - Future saveEmailAsDrafts(AccountId accountId, Email email) { - return emailDataSource.saveEmailAsDrafts(accountId, email); + Future saveEmailAsDrafts(Session session, AccountId accountId, Email email) { + return emailDataSource[DataSourceType.network]!.saveEmailAsDrafts(session, accountId, email); } @override - Future removeEmailDrafts(AccountId accountId, EmailId emailId) { - return emailDataSource.removeEmailDrafts(accountId, emailId); + Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId) { + return emailDataSource[DataSourceType.network]!.removeEmailDrafts(session, accountId, emailId); } @override - Future updateEmailDrafts(AccountId accountId, Email newEmail, EmailId oldEmailId) { - return emailDataSource.updateEmailDrafts(accountId, newEmail, oldEmailId); + Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId) { + return emailDataSource[DataSourceType.network]!.updateEmailDrafts(session, accountId, newEmail, oldEmailId); } @override @@ -127,7 +149,7 @@ class EmailRepositoryImpl extends EmailRepository { AccountRequest accountRequest, StreamController> onReceiveController ) { - return emailDataSource.downloadAttachmentForWeb( + return emailDataSource[DataSourceType.network]!.downloadAttachmentForWeb( taskId, attachment, accountId, @@ -137,24 +159,57 @@ class EmailRepositoryImpl extends EmailRepository { } @override - Future> deleteMultipleEmailsPermanently(AccountId accountId, List emailIds) { - return emailDataSource.deleteMultipleEmailsPermanently(accountId, emailIds); + Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds) { + return emailDataSource[DataSourceType.network]!.deleteMultipleEmailsPermanently(session, accountId, emailIds); } @override - Future deleteEmailPermanently(AccountId accountId, EmailId emailId) { - return emailDataSource.deleteEmailPermanently(accountId, emailId); + Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId) { + return emailDataSource[DataSourceType.network]!.deleteEmailPermanently(session, accountId, emailId); } @override - Future> addTooltipWhenHoverOnLink(List emailContents) { - return Future.wait(emailContents - .map((emailContent) => _htmlDataSource.addTooltipWhenHoverOnLink(emailContent)) - .toList()); + Future getEmailState(Session session, AccountId accountId) { + return _stateDataSource.getState(accountId, session.username, StateType.email); + } + + @override + Future storeDetailedNewEmail(Session session, AccountId accountId, DetailedEmail detailedEmail) { + return emailDataSource[DataSourceType.hiveCache]!.storeDetailedNewEmail(session, accountId, detailedEmail); + } + + @override + Future> getListDetailedEmailById(Session session, AccountId accountId, Set emailIds, {Set? sort}) { + return emailDataSource[DataSourceType.network]!.getListDetailedEmailById(session, accountId, emailIds, sort: sort); + } + + @override + Future storeEmail(Session session, AccountId accountId, Email email) { + return emailDataSource[DataSourceType.hiveCache]!.storeEmail(session, accountId, email); + } + + @override + Future storeOpenedEmail(Session session, AccountId accountId, DetailedEmail detailedEmail) { + return emailDataSource[DataSourceType.hiveCache]!.storeOpenedEmail(session, accountId, detailedEmail); + } + + @override + Future getStoredOpenedEmail(Session session, AccountId accountId, EmailId emailId) async { + return emailDataSource[DataSourceType.hiveCache]!.getStoredOpenedEmail(session, accountId, emailId); + } + + @override + Future getStoredEmail(Session session, AccountId accountId, EmailId emailId) { + return emailDataSource[DataSourceType.hiveCache]!.getStoredEmail(session, accountId, emailId); + } + + @override + Future getStoredNewEmail(Session session, AccountId accountId, EmailId emailId) { + return emailDataSource[DataSourceType.hiveCache]!.getStoredNewEmail(session, accountId, emailId); } @override - Future getEmailState() { - return _stateDataSource.getState(StateType.email); + Future transformHtmlEmailContent(String htmlContent, TransformConfiguration configuration) { + return _htmlDataSource.transformHtmlEmailContent(htmlContent, configuration); } } \ No newline at end of file diff --git a/lib/features/email/domain/exceptions/calendar_event_exceptions.dart b/lib/features/email/domain/exceptions/calendar_event_exceptions.dart new file mode 100644 index 0000000000..1129d36f55 --- /dev/null +++ b/lib/features/email/domain/exceptions/calendar_event_exceptions.dart @@ -0,0 +1,4 @@ + +class NotFoundCalendarEventException implements Exception {} + +class NotParsableCalendarEventException implements Exception {} \ No newline at end of file diff --git a/lib/features/email/domain/exceptions/email_cache_exceptions.dart b/lib/features/email/domain/exceptions/email_cache_exceptions.dart new file mode 100644 index 0000000000..5e2a9e9bdb --- /dev/null +++ b/lib/features/email/domain/exceptions/email_cache_exceptions.dart @@ -0,0 +1,8 @@ + +class NotFoundStoredOpenedEmailException implements Exception {} + +class NotFoundStoredNewEmailException implements Exception {} + +class NotFoundStoredEmailException implements Exception {} + +class OpenedEmailAlreadyStoredException implements Exception {} \ No newline at end of file diff --git a/lib/features/email/domain/exceptions/email_exceptions.dart b/lib/features/email/domain/exceptions/email_exceptions.dart new file mode 100644 index 0000000000..3fd9b5d92a --- /dev/null +++ b/lib/features/email/domain/exceptions/email_exceptions.dart @@ -0,0 +1,5 @@ +class NotFoundEmailException implements Exception {} + +class NotFoundEmailContentException implements Exception {} + +class EmptyEmailContentException implements Exception {} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/attachment_extension.dart b/lib/features/email/domain/extensions/attachment_extension.dart new file mode 100644 index 0000000000..76a87755e5 --- /dev/null +++ b/lib/features/email/domain/extensions/attachment_extension.dart @@ -0,0 +1,17 @@ + +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/attachment_hive_cache.dart'; + +extension AttachmentExtension on Attachment { + AttachmentHiveCache toHiveCache() { + return AttachmentHiveCache( + partId: partId?.value, + blobId: blobId?.value, + size: size?.value.toInt(), + name: name, + type: type?.mimeType, + cid: cid, + disposition: disposition?.name + ); + } +} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/attachment_hive_cache_extension.dart b/lib/features/email/domain/extensions/attachment_hive_cache_extension.dart new file mode 100644 index 0000000000..7d52a1c870 --- /dev/null +++ b/lib/features/email/domain/extensions/attachment_hive_cache_extension.dart @@ -0,0 +1,20 @@ +import 'package:http_parser/http_parser.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/attachment_hive_cache.dart'; + +extension AttachmentExtension on AttachmentHiveCache { + Attachment toAttachment() { + return Attachment( + partId: partId != null ? PartId(partId!) : null, + blobId: blobId != null ? Id(blobId!) : null, + size: size != null ? UnsignedInt(size!) : null, + name: name, + type: type != null ? MediaType.parse(type!) : null, + cid: cid, + disposition: disposition?.toContentDisposition(), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/detailed_email_extension.dart b/lib/features/email/domain/extensions/detailed_email_extension.dart new file mode 100644 index 0000000000..c7b9396db9 --- /dev/null +++ b/lib/features/email/domain/extensions/detailed_email_extension.dart @@ -0,0 +1,41 @@ + +import 'package:model/extensions/email_id_extensions.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/list_email_header_extension.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/detailed_email_hive_cache.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/map_keywords_extension.dart'; + +extension DetailedEmailExtension on DetailedEmail { + DetailedEmailHiveCache toHiveCache() { + return DetailedEmailHiveCache( + emailId: emailId.asString, + timeSaved: createdTime, + attachments: attachments?.toHiveCache(), + headers: headers?.toList().toHiveCache(), + keywords: keywords?.toMapString(), + emailContentPath: emailContentPath, + messageId: messageId?.ids.toList(), + references: references?.ids.toList(), + ); + } + + String get newEmailFolderPath => CachingConstants.newEmailsContentFolderName; + + String get openedEmailFolderPath => CachingConstants.openedEmailContentFolderName; + + DetailedEmail fromEmailContentPath(String path) { + return DetailedEmail( + emailId: emailId, + createdTime: createdTime, + attachments: attachments, + headers: headers, + keywords: keywords, + htmlEmailContent: htmlEmailContent, + emailContentPath: path, + messageId: messageId, + references: references, + ); + } +} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/detailed_email_hive_cache_extension.dart b/lib/features/email/domain/extensions/detailed_email_hive_cache_extension.dart new file mode 100644 index 0000000000..1a32803059 --- /dev/null +++ b/lib/features/email/domain/extensions/detailed_email_hive_cache_extension.dart @@ -0,0 +1,28 @@ +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/list_acttachments_hive_cache_extension.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/list_email_header_hive_cache_extension.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/detailed_email_hive_cache.dart'; + +extension DetailedEmailHiveCacheExtension on DetailedEmailHiveCache { + DetailedEmail toDetailedEmailWithContent(String emailContent) { + return DetailedEmail( + emailId: EmailId(Id(emailId)), + createdTime: timeSaved, + attachments: attachments?.toAttachment(), + headers: headers?.toSetEmailHeader(), + keywords: keywords != null + ? Map.fromIterables(keywords!.keys.map((value) => KeyWordIdentifier(value)), keywords!.values) + : null, + htmlEmailContent: emailContent, + messageId: messageId != null + ? MessageIdsHeaderValue(messageId!.toSet()) + : null, + references: references != null + ? MessageIdsHeaderValue(references!.toSet()) + : null + ); + } +} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/email_extension.dart b/lib/features/email/domain/extensions/email_extension.dart new file mode 100644 index 0000000000..d7df25e630 --- /dev/null +++ b/lib/features/email/domain/extensions/email_extension.dart @@ -0,0 +1,19 @@ + +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/extensions/email_extension.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; + +extension EmailExtension on Email { + DetailedEmail toDetailedEmail({String? htmlEmailContent}) { + return DetailedEmail( + emailId: id!, + createdTime: receivedAt?.value ?? DateTime.now(), + attachments: allAttachments, + headers: headers, + keywords: keywords, + htmlEmailContent: htmlEmailContent, + messageId: messageId, + references: references + ); + } +} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/email_header_extension.dart b/lib/features/email/domain/extensions/email_header_extension.dart new file mode 100644 index 0000000000..77e0493743 --- /dev/null +++ b/lib/features/email/domain/extensions/email_header_extension.dart @@ -0,0 +1,7 @@ + +import 'package:jmap_dart_client/jmap/mail/email/email_header.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/email_header_hive_cache.dart'; + +extension EmailHeaderExtension on EmailHeader { + EmailHeaderHiveCache toHiveCache() => EmailHeaderHiveCache(name: name, value: value); +} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/email_header_hive_cache_extension.dart b/lib/features/email/domain/extensions/email_header_hive_cache_extension.dart new file mode 100644 index 0000000000..29255cadc5 --- /dev/null +++ b/lib/features/email/domain/extensions/email_header_hive_cache_extension.dart @@ -0,0 +1,6 @@ +import 'package:jmap_dart_client/jmap/mail/email/email_header.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/email_header_hive_cache.dart'; + +extension EmailHeaderHiveCacheExtension on EmailHeaderHiveCache { + EmailHeader toEmailHeader() => EmailHeader(name, value); +} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/list_acttachments_hive_cache_extension.dart b/lib/features/email/domain/extensions/list_acttachments_hive_cache_extension.dart new file mode 100644 index 0000000000..f18ec07d14 --- /dev/null +++ b/lib/features/email/domain/extensions/list_acttachments_hive_cache_extension.dart @@ -0,0 +1,7 @@ +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/attachment_hive_cache_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/attachment_hive_cache.dart'; + +extension ListAttachmentsHiveCacheExtension on List { + List toAttachment() => map((attachmentHiveCache) => attachmentHiveCache.toAttachment()).toList(); +} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/list_attachments_extension.dart b/lib/features/email/domain/extensions/list_attachments_extension.dart new file mode 100644 index 0000000000..4bfa32c976 --- /dev/null +++ b/lib/features/email/domain/extensions/list_attachments_extension.dart @@ -0,0 +1,22 @@ + +import 'package:collection/collection.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/attachment_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/attachment_hive_cache.dart'; + +extension ListAttachmentsExtension on List { + List toHiveCache() => map((attachment) => attachment.toHiveCache()).toList(); + + Set get calendarEventBlobIds => subtypeICSBlobIds.isEmpty ? subtypeCalendarBlobIds : subtypeICSBlobIds; + + Set get subtypeICSBlobIds => where((attachment) => attachment.type?.subtype == Attachment.eventICSSubtype) + .map((attachment) => attachment.blobId) + .whereNotNull() + .toSet(); + + Set get subtypeCalendarBlobIds => where((attachment) => attachment.type?.subtype == Attachment.eventCalendarSubtype) + .map((attachment) => attachment.blobId) + .whereNotNull() + .toSet(); +} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/list_email_header_extension.dart b/lib/features/email/domain/extensions/list_email_header_extension.dart new file mode 100644 index 0000000000..f1cd027754 --- /dev/null +++ b/lib/features/email/domain/extensions/list_email_header_extension.dart @@ -0,0 +1,8 @@ + +import 'package:jmap_dart_client/jmap/mail/email/email_header.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/email_header_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/email_header_hive_cache.dart'; + +extension ListEmailHeaderExtension on List { + List toHiveCache() => map((emailHeader) => emailHeader.toHiveCache()).toList(); +} \ No newline at end of file diff --git a/lib/features/email/domain/extensions/list_email_header_hive_cache_extension.dart b/lib/features/email/domain/extensions/list_email_header_hive_cache_extension.dart new file mode 100644 index 0000000000..853d4d3603 --- /dev/null +++ b/lib/features/email/domain/extensions/list_email_header_hive_cache_extension.dart @@ -0,0 +1,7 @@ +import 'package:jmap_dart_client/jmap/mail/email/email_header.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/email_header_hive_cache_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/email_header_hive_cache.dart'; + +extension ListEmailHeaderExtension on List { + Set toSetEmailHeader() => map((emailHeaderHiveCache) => emailHeaderHiveCache.toEmailHeader()).toSet(); +} \ No newline at end of file diff --git a/lib/features/email/domain/model/detailed_email.dart b/lib/features/email/domain/model/detailed_email.dart new file mode 100644 index 0000000000..008e19df7c --- /dev/null +++ b/lib/features/email/domain/model/detailed_email.dart @@ -0,0 +1,42 @@ +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_header.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:model/email/attachment.dart'; + +class DetailedEmail with EquatableMixin { + final EmailId emailId; + final List? attachments; + final Set? headers; + final Map? keywords; + final String? htmlEmailContent; + final String? emailContentPath; + final DateTime createdTime; + final MessageIdsHeaderValue? messageId; + final MessageIdsHeaderValue? references; + + DetailedEmail({ + required this.emailId, + required this.createdTime, + this.attachments, + this.headers, + this.keywords, + this.htmlEmailContent, + this.emailContentPath, + this.messageId, + this.references, + }); + + @override + List get props => [ + emailId, + createdTime, + attachments, + headers, + keywords, + htmlEmailContent, + emailContentPath, + messageId, + references, + ]; +} \ No newline at end of file diff --git a/lib/features/email/domain/model/event_action.dart b/lib/features/email/domain/model/event_action.dart new file mode 100644 index 0000000000..e2368bb645 --- /dev/null +++ b/lib/features/email/domain/model/event_action.dart @@ -0,0 +1,31 @@ + +import 'package:equatable/equatable.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +enum EventActionType { + yes, + maybe, + no; + + String getLabelButton(BuildContext context) { + switch(this) { + case EventActionType.yes: + return AppLocalizations.of(context).yes; + case EventActionType.maybe: + return AppLocalizations.of(context).maybe; + case EventActionType.no: + return AppLocalizations.of(context).no; + } + } +} + +class EventAction with EquatableMixin { + final EventActionType actionType; + final String link; + + EventAction(this.actionType, this.link); + + @override + List get props => [actionType, link]; +} \ No newline at end of file diff --git a/lib/features/email/domain/model/mark_read_action.dart b/lib/features/email/domain/model/mark_read_action.dart new file mode 100644 index 0000000000..ab4b06b7cd --- /dev/null +++ b/lib/features/email/domain/model/mark_read_action.dart @@ -0,0 +1,5 @@ +enum MarkReadAction { + tap, + swipeOnThread, + undo +} \ No newline at end of file diff --git a/lib/features/email/domain/repository/calendar_event_repository.dart b/lib/features/email/domain/repository/calendar_event_repository.dart new file mode 100644 index 0000000000..809293bcdc --- /dev/null +++ b/lib/features/email/domain/repository/calendar_event_repository.dart @@ -0,0 +1,11 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; + +abstract class CalendarEventRepository { + Future>> parse(AccountId accountId, Set blobIds); + + Future> getListEventAction(String emailContents); +} \ No newline at end of file diff --git a/lib/features/email/domain/repository/email_repository.dart b/lib/features/email/domain/repository/email_repository.dart index db79b31c2f..9f319efb91 100644 --- a/lib/features/email/domain/repository/email_repository.dart +++ b/lib/features/email/domain/repository/email_repository.dart @@ -1,23 +1,39 @@ import 'dart:async'; import 'dart:typed_data'; -import 'package:core/core.dart'; +import 'package:core/data/network/download/downloaded_response.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -import 'package:model/model.dart'; +import 'package:model/account/account_request.dart'; +import 'package:model/download/download_task_id.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/email/email_content.dart'; +import 'package:model/email/mark_star_action.dart'; +import 'package:model/email/read_actions.dart'; import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; abstract class EmailRepository { - Future getEmailContent(AccountId accountId, EmailId emailId); + Future getEmailContent(Session session, AccountId accountId, EmailId emailId); - Future sendEmail(AccountId accountId, EmailRequest emailRequest, {CreateNewMailboxRequest? mailboxRequest}); + Future sendEmail( + Session session, + AccountId accountId, + EmailRequest emailRequest, + {CreateNewMailboxRequest? mailboxRequest} + ); - Future> markAsRead(AccountId accountId, List emails, ReadActions readActions); + Future> markAsRead(Session session, AccountId accountId, List emails, ReadActions readActions); Future> downloadAttachments( List attachments, @@ -43,9 +59,10 @@ abstract class EmailRepository { StreamController> onReceiveController ); - Future> moveToMailbox(AccountId accountId, MoveToMailboxRequest moveRequest); + Future> moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest); Future> markAsStar( + Session session, AccountId accountId, List emails, MarkStarAction markStarAction @@ -53,22 +70,38 @@ abstract class EmailRepository { Future> transformEmailContent( List emailContents, - List attachmentInlines, - String? baseUrlDownload, - AccountId accountId + Map mapCidImageDownloadUrl, + TransformConfiguration transformConfiguration ); - Future> addTooltipWhenHoverOnLink(List emailContents); + Future saveEmailAsDrafts(Session session, AccountId accountId, Email email); + + Future removeEmailDrafts(Session session, AccountId accountId, EmailId emailId); + + Future updateEmailDrafts(Session session, AccountId accountId, Email newEmail, EmailId oldEmailId); + + Future> deleteMultipleEmailsPermanently(Session session, AccountId accountId, List emailIds); - Future saveEmailAsDrafts(AccountId accountId, Email email); + Future deleteEmailPermanently(Session session, AccountId accountId, EmailId emailId); - Future removeEmailDrafts(AccountId accountId, EmailId emailId); + Future getEmailState(Session session, AccountId accountId); - Future updateEmailDrafts(AccountId accountId, Email newEmail, EmailId oldEmailId); + Future storeDetailedNewEmail(Session session, AccountId accountId, DetailedEmail detailedEmail); - Future> deleteMultipleEmailsPermanently(AccountId accountId, List emailIds); + Future> getListDetailedEmailById(Session session, AccountId accountId, Set emailIds, {Set? sort}); - Future deleteEmailPermanently(AccountId accountId, EmailId emailId); + Future storeEmail(Session session, AccountId accountId, Email email); - Future getEmailState(); + Future getStoredEmail(Session session, AccountId accountId, EmailId emailId); + + Future storeOpenedEmail(Session session, AccountId accountId, DetailedEmail detailedEmail); + + Future getStoredOpenedEmail(Session session, AccountId accountId, EmailId emailId); + + Future getStoredNewEmail(Session session, AccountId accountId, EmailId emailId); + + Future transformHtmlEmailContent( + String htmlContent, + TransformConfiguration configuration + ); } \ No newline at end of file diff --git a/lib/features/email/domain/state/delete_email_permanently_state.dart b/lib/features/email/domain/state/delete_email_permanently_state.dart index de50c93d2f..9cd56e1e63 100644 --- a/lib/features/email/domain/state/delete_email_permanently_state.dart +++ b/lib/features/email/domain/state/delete_email_permanently_state.dart @@ -1,14 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; -class StartDeleteEmailPermanently extends UIState { - - StartDeleteEmailPermanently(); - - @override - List get props => []; -} +class StartDeleteEmailPermanently extends UIState {} class DeleteEmailPermanentlySuccess extends UIActionState { @@ -16,17 +11,9 @@ class DeleteEmailPermanentlySuccess extends UIActionState { jmap.State? currentEmailState, jmap.State? currentMailboxState, }) : super(currentEmailState, currentMailboxState); - - @override - List get props => []; } class DeleteEmailPermanentlyFailure extends FeatureFailure { - final dynamic exception; - - DeleteEmailPermanentlyFailure(this.exception); - - @override - List get props => [exception]; + DeleteEmailPermanentlyFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart b/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart index 9b1c014ade..33b9b84671 100644 --- a/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart +++ b/lib/features/email/domain/state/delete_multiple_emails_permanently_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; @@ -18,7 +19,7 @@ class DeleteMultipleEmailsPermanentlyAllSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [emailIds]; + List get props => [emailIds, ...super.props]; } class DeleteMultipleEmailsPermanentlyHasSomeEmailFailure extends UIActionState { @@ -34,23 +35,12 @@ class DeleteMultipleEmailsPermanentlyHasSomeEmailFailure extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [emailIds]; + List get props => [emailIds, ...super.props]; } -class DeleteMultipleEmailsPermanentlyAllFailure extends FeatureFailure { - - DeleteMultipleEmailsPermanentlyAllFailure(); - - @override - List get props => []; -} +class DeleteMultipleEmailsPermanentlyAllFailure extends FeatureFailure {} class DeleteMultipleEmailsPermanentlyFailure extends FeatureFailure { - final dynamic exception; - - DeleteMultipleEmailsPermanentlyFailure(this.exception); - - @override - List get props => [exception]; + DeleteMultipleEmailsPermanentlyFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/email/domain/state/delete_sending_email_state.dart b/lib/features/email/domain/state/delete_sending_email_state.dart new file mode 100644 index 0000000000..4ba12eaa7e --- /dev/null +++ b/lib/features/email/domain/state/delete_sending_email_state.dart @@ -0,0 +1,10 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class DeleteSendingEmailLoading extends UIState {} + +class DeleteSendingEmailSuccess extends UIState {} + +class DeleteSendingEmailFailure extends FeatureFailure { + DeleteSendingEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/email/domain/state/download_attachment_for_web_state.dart b/lib/features/email/domain/state/download_attachment_for_web_state.dart index 2c62ef9b05..f6822d8873 100644 --- a/lib/features/email/domain/state/download_attachment_for_web_state.dart +++ b/lib/features/email/domain/state/download_attachment_for_web_state.dart @@ -1,7 +1,8 @@ import 'dart:typed_data'; - -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/download/download_task_id.dart'; +import 'package:model/email/attachment.dart'; class StartDownloadAttachmentForWeb extends UIState { @@ -55,10 +56,9 @@ class DownloadAttachmentForWebSuccess extends UIState { class DownloadAttachmentForWebFailure extends FeatureFailure { final DownloadTaskId taskId; - final dynamic exception; - DownloadAttachmentForWebFailure(this.taskId, this.exception); + DownloadAttachmentForWebFailure(this.taskId, dynamic exception) : super(exception: exception); @override - List get props => [taskId, exception]; + List get props => [taskId, ...super.props]; } \ No newline at end of file diff --git a/lib/features/email/domain/state/download_attachments_state.dart b/lib/features/email/domain/state/download_attachments_state.dart index 3e7f51e986..42f2e6a997 100644 --- a/lib/features/email/domain/state/download_attachments_state.dart +++ b/lib/features/email/domain/state/download_attachments_state.dart @@ -1,5 +1,6 @@ -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/download/download_task_id.dart'; class DownloadAttachmentsSuccess extends UIState { final List taskIds; @@ -11,10 +12,6 @@ class DownloadAttachmentsSuccess extends UIState { } class DownloadAttachmentsFailure extends FeatureFailure { - final exception; - DownloadAttachmentsFailure(this.exception); - - @override - List get props => [exception]; + DownloadAttachmentsFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/email/domain/state/export_attachment_state.dart b/lib/features/email/domain/state/export_attachment_state.dart index 048f830bbf..75ee962171 100644 --- a/lib/features/email/domain/state/export_attachment_state.dart +++ b/lib/features/email/domain/state/export_attachment_state.dart @@ -1,4 +1,6 @@ -import 'package:core/core.dart'; +import 'package:core/data/network/download/downloaded_response.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; class ExportAttachmentSuccess extends UIState { final DownloadedResponse downloadedResponse; @@ -10,10 +12,6 @@ class ExportAttachmentSuccess extends UIState { } class ExportAttachmentFailure extends FeatureFailure { - final dynamic exception; - ExportAttachmentFailure(this.exception); - - @override - List get props => [exception]; + ExportAttachmentFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/email/domain/state/get_detailed_email_by_id_state.dart b/lib/features/email/domain/state/get_detailed_email_by_id_state.dart new file mode 100644 index 0000000000..feaa71aec3 --- /dev/null +++ b/lib/features/email/domain/state/get_detailed_email_by_id_state.dart @@ -0,0 +1,25 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; + +class GetDetailedEmailByIdLoading extends UIState {} + +class GetDetailedEmailByIdSuccess extends UIState { + + final Map mapDetailedEmail; + final AccountId accountId; + final Session session; + + GetDetailedEmailByIdSuccess(this.mapDetailedEmail, this.accountId, this.session); + + @override + List get props => [mapDetailedEmail, accountId, session]; +} + +class GetDetailedEmailByIdFailure extends FeatureFailure { + + GetDetailedEmailByIdFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/email/domain/state/get_email_content_state.dart b/lib/features/email/domain/state/get_email_content_state.dart index d35671ff65..9742216d9c 100644 --- a/lib/features/email/domain/state/get_email_content_state.dart +++ b/lib/features/email/domain/state/get_email_content_state.dart @@ -1,36 +1,49 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:model/model.dart'; +import 'package:model/email/attachment.dart'; class GetEmailContentLoading extends LoadingState {} class GetEmailContentSuccess extends UIState { - final List emailContents; - final List emailContentsDisplayed; + final String htmlEmailContent; final List attachments; final Email? emailCurrent; - GetEmailContentSuccess( - this.emailContents, - this.emailContentsDisplayed, - this.attachments, + GetEmailContentSuccess({ + required this.htmlEmailContent, + required this.attachments, this.emailCurrent - ); + }); @override List get props => [ - emailContents, - emailContentsDisplayed, + htmlEmailContent, attachments, emailCurrent ]; } -class GetEmailContentFailure extends FeatureFailure { - final dynamic exception; +class GetEmailContentFromCacheSuccess extends UIState { + final String htmlEmailContent; + final List attachments; + final Email? emailCurrent; - GetEmailContentFailure(this.exception); + GetEmailContentFromCacheSuccess({ + required this.htmlEmailContent, + required this.attachments, + this.emailCurrent + }); @override - List get props => [exception]; + List get props => [ + htmlEmailContent, + attachments, + emailCurrent, + ]; +} + +class GetEmailContentFailure extends FeatureFailure { + + GetEmailContentFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/email/domain/state/get_stored_state_email_state.dart b/lib/features/email/domain/state/get_stored_state_email_state.dart index 63612704ac..ad5b374b5c 100644 --- a/lib/features/email/domain/state/get_stored_state_email_state.dart +++ b/lib/features/email/domain/state/get_stored_state_email_state.dart @@ -13,19 +13,9 @@ class GetStoredEmailStateSuccess extends UIState { List get props => [state]; } -class NotFoundEmailState extends FeatureFailure { - - NotFoundEmailState(); - - @override - List get props => []; -} +class NotFoundEmailState extends FeatureFailure {} class GetStoredEmailStateFailure extends FeatureFailure { - final dynamic exception; - - GetStoredEmailStateFailure(this.exception); - @override - List get props => [exception]; + GetStoredEmailStateFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/email/domain/state/mark_as_email_read_state.dart b/lib/features/email/domain/state/mark_as_email_read_state.dart index 968c8645b3..28e48586cb 100644 --- a/lib/features/email/domain/state/mark_as_email_read_state.dart +++ b/lib/features/email/domain/state/mark_as_email_read_state.dart @@ -1,17 +1,20 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/email/read_actions.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; +import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; class MarkAsEmailReadSuccess extends UIActionState { final Email updatedEmail; final ReadActions readActions; + final MarkReadAction markReadAction; MarkAsEmailReadSuccess( this.updatedEmail, this.readActions, + this.markReadAction, { jmap.State? currentEmailState, jmap.State? currentMailboxState, @@ -19,15 +22,14 @@ class MarkAsEmailReadSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [updatedEmail, readActions]; + List get props => [updatedEmail, readActions, markReadAction, ...super.props]; } class MarkAsEmailReadFailure extends FeatureFailure { - final dynamic exception; final ReadActions readActions; - MarkAsEmailReadFailure(this.exception, this.readActions); + MarkAsEmailReadFailure(this.readActions, {dynamic exception}) : super(exception: exception); @override - List get props => [exception, readActions]; + List get props => [readActions, ...super.props]; } \ No newline at end of file diff --git a/lib/features/email/domain/state/mark_as_email_star_state.dart b/lib/features/email/domain/state/mark_as_email_star_state.dart index cf42559efe..dbe215734d 100644 --- a/lib/features/email/domain/state/mark_as_email_star_state.dart +++ b/lib/features/email/domain/state/mark_as_email_star_state.dart @@ -1,4 +1,4 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; @@ -18,15 +18,14 @@ class MarkAsStarEmailSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [updatedEmail, markStarAction]; + List get props => [updatedEmail, markStarAction, ...super.props]; } class MarkAsStarEmailFailure extends FeatureFailure { - final dynamic exception; final MarkStarAction markStarAction; - MarkAsStarEmailFailure(this.exception, this.markStarAction); + MarkAsStarEmailFailure(this.markStarAction, {dynamic exception}) : super(exception: exception); @override - List get props => [exception, markStarAction]; + List get props => [markStarAction, ...super.props]; } \ No newline at end of file diff --git a/lib/features/email/domain/state/move_to_mailbox_state.dart b/lib/features/email/domain/state/move_to_mailbox_state.dart index 482c2928dc..66e1359d49 100644 --- a/lib/features/email/domain/state/move_to_mailbox_state.dart +++ b/lib/features/email/domain/state/move_to_mailbox_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; @@ -36,16 +37,16 @@ class MoveToMailboxSuccess extends UIActionState { destinationMailboxId, moveAction, emailActionType, - destinationPath + destinationPath, + ...super.props ]; } class MoveToMailboxFailure extends FeatureFailure { final EmailActionType emailActionType; - final dynamic exception; - MoveToMailboxFailure(this.emailActionType, this.exception); + MoveToMailboxFailure(this.emailActionType, {dynamic exception}) : super(exception: exception); @override - List get props => [emailActionType, exception]; + List get props => [emailActionType, ...super.props]; } \ No newline at end of file diff --git a/lib/features/email/domain/state/parse_calendar_event_state.dart b/lib/features/email/domain/state/parse_calendar_event_state.dart new file mode 100644 index 0000000000..769f20e8d6 --- /dev/null +++ b/lib/features/email/domain/state/parse_calendar_event_state.dart @@ -0,0 +1,21 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; + +class ParseCalendarEventLoading extends LoadingState {} + +class ParseCalendarEventSuccess extends UIState { + + final List calendarEventList; + final List eventActionList; + + ParseCalendarEventSuccess(this.calendarEventList, this.eventActionList); + + @override + List get props => [calendarEventList, eventActionList]; +} + +class ParseCalendarEventFailure extends FeatureFailure { + ParseCalendarEventFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/email/domain/state/send_receipt_to_sender_state.dart b/lib/features/email/domain/state/send_receipt_to_sender_state.dart index 08cb4ad834..fd29e08af0 100644 --- a/lib/features/email/domain/state/send_receipt_to_sender_state.dart +++ b/lib/features/email/domain/state/send_receipt_to_sender_state.dart @@ -16,10 +16,5 @@ class SendReceiptToSenderSuccess extends UIState { class SendReceiptToSenderFailure extends FeatureFailure { - final dynamic exception; - - SendReceiptToSenderFailure(this.exception); - - @override - List get props => [exception]; + SendReceiptToSenderFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/email/domain/state/store_new_email_state.dart b/lib/features/email/domain/state/store_new_email_state.dart new file mode 100644 index 0000000000..e0acc2f8be --- /dev/null +++ b/lib/features/email/domain/state/store_new_email_state.dart @@ -0,0 +1,11 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class StoreNewEmailLoading extends UIState {} + +class StoreNewEmailSuccess extends UIState {} + +class StoreNewEmailFailure extends FeatureFailure { + + StoreNewEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/email/domain/state/store_opened_email_state.dart b/lib/features/email/domain/state/store_opened_email_state.dart new file mode 100644 index 0000000000..c479ed2d30 --- /dev/null +++ b/lib/features/email/domain/state/store_opened_email_state.dart @@ -0,0 +1,11 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class StoreOpenedEmailLoading extends UIState {} + +class StoreOpenedEmailSuccess extends UIState {} + +class StoreOpenedEmailFailure extends FeatureFailure { + + StoreOpenedEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/email/domain/state/store_sending_email_state.dart b/lib/features/email/domain/state/store_sending_email_state.dart new file mode 100644 index 0000000000..a3fb262786 --- /dev/null +++ b/lib/features/email/domain/state/store_sending_email_state.dart @@ -0,0 +1,20 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +class StoreSendingEmailLoading extends UIState {} + +class StoreSendingEmailSuccess extends UIState { + + final SendingEmail sendingEmail; + + StoreSendingEmailSuccess(this.sendingEmail); + + @override + List get props => [sendingEmail]; +} + +class StoreSendingEmailFailure extends FeatureFailure { + StoreSendingEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/email/domain/state/transform_html_email_content_state.dart b/lib/features/email/domain/state/transform_html_email_content_state.dart new file mode 100644 index 0000000000..f3b70df113 --- /dev/null +++ b/lib/features/email/domain/state/transform_html_email_content_state.dart @@ -0,0 +1,18 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class TransformHtmlEmailContentLoading extends LoadingState {} + +class TransformHtmlEmailContentSuccess extends UIState { + final String htmlContent; + + TransformHtmlEmailContentSuccess(this.htmlContent); + + @override + List get props => [htmlContent]; +} + +class TransformHtmlEmailContentFailure extends FeatureFailure { + + TransformHtmlEmailContentFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart b/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart index 4cce03b57a..35433b82ca 100644 --- a/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart +++ b/lib/features/email/domain/usecases/delete_email_permanently_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; @@ -12,19 +13,19 @@ class DeleteEmailPermanentlyInteractor { DeleteEmailPermanentlyInteractor(this._emailRepository, this._mailboxRepository); - Stream> execute(AccountId accountId, EmailId emailId) async* { + Stream> execute(Session session, AccountId accountId, EmailId emailId) async* { try { yield Right(StartDeleteEmailPermanently()); final listState = await Future.wait([ - _mailboxRepository.getMailboxState(), - _emailRepository.getEmailState(), + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), ], eagerError: true); final currentMailboxState = listState.first; final currentEmailState = listState.last; - final result = await _emailRepository.deleteEmailPermanently(accountId, emailId); + final result = await _emailRepository.deleteEmailPermanently(session, accountId, emailId); if (result) { yield Right(DeleteEmailPermanentlySuccess( currentEmailState: currentEmailState, diff --git a/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart b/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart index 554e43be94..29befa5b14 100644 --- a/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart +++ b/lib/features/email/domain/usecases/delete_multiple_emails_permanently_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; @@ -12,19 +13,19 @@ class DeleteMultipleEmailsPermanentlyInteractor { DeleteMultipleEmailsPermanentlyInteractor(this._emailRepository, this._mailboxRepository); - Stream> execute(AccountId accountId, List emailIds) async* { + Stream> execute(Session session, AccountId accountId, List emailIds) async* { try { yield Right(LoadingDeleteMultipleEmailsPermanentlyAll()); final listState = await Future.wait([ - _mailboxRepository.getMailboxState(), - _emailRepository.getEmailState(), + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), ], eagerError: true); final currentMailboxState = listState.first; final currentEmailState = listState.last; - final listResult = await _emailRepository.deleteMultipleEmailsPermanently(accountId, emailIds); + final listResult = await _emailRepository.deleteMultipleEmailsPermanently(session, accountId, emailIds); if (listResult.length == emailIds.length) { yield Right(DeleteMultipleEmailsPermanentlyAllSuccess( listResult, diff --git a/lib/features/email/domain/usecases/download_attachment_for_web_interactor.dart b/lib/features/email/domain/usecases/download_attachment_for_web_interactor.dart index 9596ae82b8..eb113fd5f4 100644 --- a/lib/features/email/domain/usecases/download_attachment_for_web_interactor.dart +++ b/lib/features/email/domain/usecases/download_attachment_for_web_interactor.dart @@ -4,10 +4,10 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/account_request.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/account/password.dart'; -import 'package:model/account/user_name.dart'; import 'package:model/download/download_task_id.dart'; import 'package:model/email/attachment.dart'; import 'package:model/oidc/token_oidc.dart'; diff --git a/lib/features/email/domain/usecases/download_attachments_interactor.dart b/lib/features/email/domain/usecases/download_attachments_interactor.dart index fa15f9b7c2..e20056d7d2 100644 --- a/lib/features/email/domain/usecases/download_attachments_interactor.dart +++ b/lib/features/email/domain/usecases/download_attachments_interactor.dart @@ -1,7 +1,15 @@ -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:model/account/account_request.dart'; +import 'package:model/account/authentication_type.dart'; +import 'package:model/account/password.dart'; +import 'package:model/account/personal_account.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/download_attachments_state.dart'; import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; @@ -64,7 +72,8 @@ class DownloadAttachmentsInteractor { } } catch (exception) { log('DownloadAttachmentsInteractor::execute(): $exception'); - if (exception is DownloadAttachmentHasTokenExpiredException) { + if (exception is DownloadAttachmentHasTokenExpiredException && + exception.refreshToken.isNotEmpty) { yield* _retryDownloadAttachments( accountId, baseDownloadUrl, @@ -95,13 +104,13 @@ class DownloadAttachmentsInteractor { await Future.wait([ _authenticationOIDCRepository.persistTokenOIDC(newTokenOIDC), _accountRepository.deleteCurrentAccount(accountCurrent.id), - _accountRepository.setCurrentAccount(Account( + _accountRepository.setCurrentAccount(PersonalAccount( newTokenOIDC.tokenIdHash, AuthenticationType.oidc, isSelected: true, accountId: accountId, - apiUrl: accountCurrent.apiUrl - )) + apiUrl: accountCurrent.apiUrl, + userName: accountCurrent.userName)) ]); _authorizationInterceptors.setTokenAndAuthorityOidc( diff --git a/lib/features/email/domain/usecases/export_attachment_interactor.dart b/lib/features/email/domain/usecases/export_attachment_interactor.dart index 34a1e7608c..2696d30d74 100644 --- a/lib/features/email/domain/usecases/export_attachment_interactor.dart +++ b/lib/features/email/domain/usecases/export_attachment_interactor.dart @@ -4,6 +4,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/export_attachment_state.dart'; diff --git a/lib/features/email/domain/usecases/get_email_content_interactor.dart b/lib/features/email/domain/usecases/get_email_content_interactor.dart index 92dc0a6001..44a8e19c0a 100644 --- a/lib/features/email/domain/usecases/get_email_content_interactor.dart +++ b/lib/features/email/domain/usecases/get_email_content_interactor.dart @@ -1,7 +1,12 @@ -import 'package:core/core.dart'; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; -import 'package:flutter/foundation.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; @@ -12,35 +17,125 @@ class GetEmailContentInteractor { GetEmailContentInteractor(this.emailRepository); - Stream> execute(AccountId accountId, EmailId emailId, String? baseDownloadUrl) async* { + Stream> execute( + Session session, + AccountId accountId, + EmailId emailId, + String baseDownloadUrl, + TransformConfiguration transformConfiguration, + ) async* { try { yield Right(GetEmailContentLoading()); - final email = await emailRepository.getEmailContent(accountId, emailId); + + if (PlatformInfo.isMobile) { + yield* _getStoredOpenedEmail(session, accountId, emailId, baseDownloadUrl, transformConfiguration); + } else { + yield* _getContentEmailFromServer(session, accountId, emailId, baseDownloadUrl, transformConfiguration); + } + } catch (e) { + log('GetEmailContentInteractor::execute(): exception = $e'); + yield Left(GetEmailContentFailure(e)); + } + } + + Stream> _getContentEmailFromServer( + Session session, + AccountId accountId, + EmailId emailId, + String baseDownloadUrl, + TransformConfiguration transformConfiguration, + ) async* { + try { + final email = await emailRepository.getEmailContent(session, accountId, emailId); if (email.emailContentList.isNotEmpty) { + final mapCidImageDownloadUrl = email.attachmentsWithCid.toMapCidImageDownloadUrl( + accountId: accountId, + downloadUrl: baseDownloadUrl + ); final newEmailContents = await emailRepository.transformEmailContent( - email.emailContentList, - email.allAttachments.listAttachmentsDisplayedInContent, - baseDownloadUrl, - accountId); - final newEmailContentsDisplayed = kIsWeb - ? await emailRepository.addTooltipWhenHoverOnLink(newEmailContents) - : newEmailContents; + email.emailContentList, + mapCidImageDownloadUrl, + transformConfiguration + ); + yield Right(GetEmailContentSuccess( - newEmailContents, - newEmailContentsDisplayed, - email.allAttachments, - email)); - } else if (email.allAttachments.isNotEmpty) { - yield Right(GetEmailContentSuccess([], [], email.allAttachments, email)); - } else if (email.headers?.isNotEmpty == true) { - yield Right(GetEmailContentSuccess([], [], [], email)); + htmlEmailContent: newEmailContents.asHtmlString, + attachments: email.allAttachments, + emailCurrent: email + )); } else { - yield Left(GetEmailContentFailure(null)); + yield Right(GetEmailContentSuccess( + htmlEmailContent: '', + attachments: email.allAttachments, + emailCurrent: email + )); } } catch (e) { - log('GetEmailContentInteractor::execute(): exception = $e'); - yield Left(GetEmailContentFailure(e)); + logError('GetEmailContentInteractor::_getContentEmailFromServer():EXCEPTION: $e'); + yield Left(GetEmailContentFailure(e)); + } + } + + Stream> _getStoredOpenedEmail( + Session session, + AccountId accountId, + EmailId emailId, + String baseDownloadUrl, + TransformConfiguration transformConfiguration, + ) async* { + try { + log('GetEmailContentInteractor::_getStoredOpenedEmail(): CALLED'); + final detailedEmail = await emailRepository.getStoredOpenedEmail(session, accountId, emailId); + yield Right(GetEmailContentFromCacheSuccess( + htmlEmailContent: detailedEmail.htmlEmailContent ?? '', + attachments: detailedEmail.attachments ?? [], + emailCurrent: Email( + id: emailId, + headers: detailedEmail.headers, + keywords: detailedEmail.keywords + ) + )); + } catch (e) { + logError('GetEmailContentInteractor::_getStoredOpenedEmail():EXCEPTION: $e'); + yield* _getStoredNewEmail( + session, + accountId, + emailId, + baseDownloadUrl, + transformConfiguration + ); + } + } + + Stream> _getStoredNewEmail( + Session session, + AccountId accountId, + EmailId emailId, + String baseDownloadUrl, + TransformConfiguration transformConfiguration, + ) async* { + try { + log('GetEmailContentInteractor::_getStoredNewEmail():CALLED'); + final detailedEmail = await emailRepository.getStoredNewEmail(session, accountId, emailId); + yield Right(GetEmailContentFromCacheSuccess( + htmlEmailContent: detailedEmail.htmlEmailContent ?? '', + attachments: detailedEmail.attachments ?? [], + emailCurrent: Email( + id: emailId, + headers: detailedEmail.headers, + keywords: detailedEmail.keywords + ) + )); + } catch (e) { + logError('GetEmailContentInteractor::_getStoredNewEmail():EXCEPTION: $e'); + yield* _getContentEmailFromServer( + session, + accountId, + emailId, + baseDownloadUrl, + transformConfiguration + ); } } } \ No newline at end of file diff --git a/lib/features/email/domain/usecases/get_list_detailed_email_by_id_interator.dart b/lib/features/email/domain/usecases/get_list_detailed_email_by_id_interator.dart new file mode 100644 index 0000000000..97a5810fb4 --- /dev/null +++ b/lib/features/email/domain/usecases/get_list_detailed_email_by_id_interator.dart @@ -0,0 +1,91 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/extensions/utc_date_extension.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/extensions/email_extension.dart'; +import 'package:model/extensions/list_attachment_extension.dart'; +import 'package:model/extensions/list_email_content_extension.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/email_extension.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_detailed_email_by_id_state.dart'; + +class GetListDetailedEmailByIdInteractor { + final EmailRepository _emailRepository; + + GetListDetailedEmailByIdInteractor(this._emailRepository); + + Stream> execute( + Session session, + AccountId accountId, + Set emailIds, + String baseDownloadUrl, + {Set? sort} + ) async* { + try { + yield Right(GetDetailedEmailByIdLoading()); + + final listEmails = await _emailRepository.getListDetailedEmailById(session, accountId, emailIds, sort: sort); + + final listTuple2Email = await Future.wait( + listEmails.map((email) => _parsingEmailToDetailedEmail(accountId, email, baseDownloadUrl)), + eagerError: true); + + listTuple2Email.sort((detailedEmail1, detailedEmail2) { + return detailedEmail1.value1.receivedAt.compareToSort(detailedEmail1.value1.receivedAt, true); + }); + + final mapDetailedEmails = { + for (var tuple2 in listTuple2Email) + tuple2.value1 : tuple2.value2 + }; + + yield Right(GetDetailedEmailByIdSuccess( + mapDetailedEmails, + accountId, + session, + )); + } catch (e) { + yield Left(GetDetailedEmailByIdFailure(e)); + } + } + + Future> _parsingEmailToDetailedEmail( + AccountId accountId, + Email email, + String baseDownloadUrl + ) async { + String? htmlEmailContent; + + final listEmailContent = email.emailContentList; + if (listEmailContent.isNotEmpty) { + final mapCidImageDownloadUrl = email.attachmentsWithCid.toMapCidImageDownloadUrl( + accountId: accountId, + downloadUrl: baseDownloadUrl + ); + TransformConfiguration transformConfiguration = TransformConfiguration.forPreviewEmail(); + if (email.isDraft) { + transformConfiguration = TransformConfiguration.forDraftsEmail(); + } else if (PlatformInfo.isWeb) { + transformConfiguration = TransformConfiguration.forPreviewEmailOnWeb(); + } + final newEmailContents = await _emailRepository.transformEmailContent( + email.emailContentList, + mapCidImageDownloadUrl, + transformConfiguration + ); + + htmlEmailContent = newEmailContents.asHtmlString; + } + + final detailedEmail = email.toDetailedEmail(htmlEmailContent: htmlEmailContent); + + return Tuple2(email, detailedEmail); + } +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/get_stored_email_state_interactor.dart b/lib/features/email/domain/usecases/get_stored_email_state_interactor.dart index effd5531f6..2cdc1bd086 100644 --- a/lib/features/email/domain/usecases/get_stored_email_state_interactor.dart +++ b/lib/features/email/domain/usecases/get_stored_email_state_interactor.dart @@ -2,6 +2,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_stored_state_email_state.dart'; @@ -10,9 +11,9 @@ class GetStoredEmailStateInteractor { GetStoredEmailStateInteractor(this._emailRepository); - Stream> execute(AccountId accountId) async* { + Stream> execute(Session session, AccountId accountId) async* { try { - final state = await _emailRepository.getEmailState(); + final state = await _emailRepository.getEmailState(session, accountId); if (state != null) { yield Right(GetStoredEmailStateSuccess(state)); } else { diff --git a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart index cf605c9f7b..435f6064c5 100644 --- a/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_email_read_interactor.dart @@ -1,8 +1,10 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; @@ -13,29 +15,30 @@ class MarkAsEmailReadInteractor { MarkAsEmailReadInteractor(this._emailRepository, this._mailboxRepository); - Stream> execute(AccountId accountId, Email email, ReadActions readAction) async* { + Stream> execute(Session session, AccountId accountId, Email email, ReadActions readAction, MarkReadAction markReadAction) async* { try { final listState = await Future.wait([ - _mailboxRepository.getMailboxState(), - _emailRepository.getEmailState(), + _mailboxRepository.getMailboxState( session,accountId), + _emailRepository.getEmailState(session, accountId), ], eagerError: true); final currentMailboxState = listState.first; final currentEmailState = listState.last; - final result = await _emailRepository.markAsRead(accountId, [email], readAction); + final result = await _emailRepository.markAsRead(session, accountId, [email], readAction); if (result.isNotEmpty) { final updatedEmail = email.updatedEmail(newKeywords: result.first.keywords); yield Right(MarkAsEmailReadSuccess( updatedEmail, readAction, + markReadAction, currentEmailState: currentEmailState, currentMailboxState: currentMailboxState)); } else { - yield Left(MarkAsEmailReadFailure(null, readAction)); + yield Left(MarkAsEmailReadFailure(readAction)); } } catch (e) { - yield Left(MarkAsEmailReadFailure(e, readAction)); + yield Left(MarkAsEmailReadFailure(readAction, exception: e)); } } } \ No newline at end of file diff --git a/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart b/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart index 1b6fe178da..df89ea182f 100644 --- a/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart +++ b/lib/features/email/domain/usecases/mark_as_star_email_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; @@ -11,10 +12,10 @@ class MarkAsStarEmailInteractor { MarkAsStarEmailInteractor(this.emailRepository); - Stream> execute(AccountId accountId, Email email, MarkStarAction markStarAction) async* { + Stream> execute(Session session, AccountId accountId, Email email, MarkStarAction markStarAction) async* { try { - final currentEmailState = await emailRepository.getEmailState(); - final result = await emailRepository.markAsStar(accountId, [email], markStarAction); + final currentEmailState = await emailRepository.getEmailState(session, accountId); + final result = await emailRepository.markAsStar(session, accountId, [email], markStarAction); if (result.isNotEmpty) { final updatedEmail = email.updatedEmail(newKeywords: result.first.keywords); yield Right(MarkAsStarEmailSuccess( @@ -22,10 +23,10 @@ class MarkAsStarEmailInteractor { markStarAction, currentEmailState: currentEmailState)); } else { - yield Left(MarkAsStarEmailFailure(null, markStarAction)); + yield Left(MarkAsStarEmailFailure(markStarAction)); } } catch (e) { - yield Left(MarkAsStarEmailFailure(e, markStarAction)); + yield Left(MarkAsStarEmailFailure(markStarAction, exception: e)); } } } \ No newline at end of file diff --git a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart index 7a95884837..6c073dd740 100644 --- a/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart +++ b/lib/features/email/domain/usecases/move_to_mailbox_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; @@ -12,19 +13,19 @@ class MoveToMailboxInteractor { MoveToMailboxInteractor(this._emailRepository, this._mailboxRepository); - Stream> execute(AccountId accountId, MoveToMailboxRequest moveRequest) async* { + Stream> execute(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) async* { try { yield Right(LoadingMoveToMailbox()); final listState = await Future.wait([ - _mailboxRepository.getMailboxState(), - _emailRepository.getEmailState(), + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), ], eagerError: true); final currentMailboxState = listState.first; final currentEmailState = listState.last; - final result = await _emailRepository.moveToMailbox(accountId, moveRequest); + final result = await _emailRepository.moveToMailbox(session, accountId, moveRequest); if (result.isNotEmpty) { yield Right(MoveToMailboxSuccess( result.first, @@ -36,10 +37,10 @@ class MoveToMailboxInteractor { currentMailboxState: currentMailboxState, currentEmailState: currentEmailState)); } else { - yield Left(MoveToMailboxFailure(moveRequest.emailActionType, null)); + yield Left(MoveToMailboxFailure(moveRequest.emailActionType)); } } catch (e) { - yield Left(MoveToMailboxFailure(moveRequest.emailActionType, e)); + yield Left(MoveToMailboxFailure(moveRequest.emailActionType, exception: e)); } } } \ No newline at end of file diff --git a/lib/features/email/domain/usecases/parse_calendar_event_interactor.dart b/lib/features/email/domain/usecases/parse_calendar_event_interactor.dart new file mode 100644 index 0000000000..e40e43dfaf --- /dev/null +++ b/lib/features/email/domain/usecases/parse_calendar_event_interactor.dart @@ -0,0 +1,55 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/calendar_event_exceptions.dart'; +import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/calendar_event_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart'; + +class ParseCalendarEventInteractor { + final CalendarEventRepository _calendarEventRepository; + + ParseCalendarEventInteractor(this._calendarEventRepository); + + Stream> execute( + AccountId accountId, + Set blobIds, + String emailContents + ) async* { + try { + yield Right(ParseCalendarEventLoading()); + + final listResult = await Future.wait( + [ + _calendarEventRepository.parse(accountId, blobIds), + _calendarEventRepository.getListEventAction(emailContents), + ], + eagerError: true + ); + + final listCalendarEvent = List.empty(growable: true); + final listEventAction = List.empty(growable: true); + + if (listResult[0] is Map>) { + final mapCalendarEvent = listResult[0] as Map>; + for (var calendarEvents in mapCalendarEvent.values) { + listCalendarEvent.addAll(calendarEvents); + } + } + if (listResult[1] is List) { + listEventAction.addAll(listResult[1] as List); + } + + if (listCalendarEvent.isNotEmpty) { + yield Right(ParseCalendarEventSuccess(listCalendarEvent, listEventAction)); + } else { + yield Left(ParseCalendarEventFailure(NotFoundCalendarEventException())); + } + } catch (e) { + yield Left(ParseCalendarEventFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/store_list_new_email_interator.dart b/lib/features/email/domain/usecases/store_list_new_email_interator.dart new file mode 100644 index 0000000000..4b616d2e27 --- /dev/null +++ b/lib/features/email/domain/usecases/store_list_new_email_interator.dart @@ -0,0 +1,64 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/store_new_email_state.dart'; + +class StoreListNewEmailInteractor { + final EmailRepository _emailRepository; + + StoreListNewEmailInteractor(this._emailRepository); + + Stream> execute( + Session session, + AccountId accountId, + Map mapDetailedEmails + ) async* { + try { + yield Right(StoreNewEmailLoading()); + + for (var email in mapDetailedEmails.keys) { + await _storeNewEmail(session, accountId, email, mapDetailedEmails[email]!); + } + + yield Right(StoreNewEmailSuccess()); + } catch (e) { + yield Left(StoreNewEmailFailure(e)); + } + } + + Future _isNewEmailAlreadyStored( + Session session, + AccountId accountId, + DetailedEmail detailedEmail + ) async { + try { + await _emailRepository.getStoredNewEmail(session, accountId, detailedEmail.emailId); + return true; + } catch (err) { + logError('StoreNewEmailInteractor::_isNewEmailAlreadyStored():EXCEPTION: $err'); + return false; + } + } + + Future _storeNewEmail( + Session session, + AccountId accountId, + Email email, + DetailedEmail detailedEmail + ) async { + final isNewEmailExist = await _isNewEmailAlreadyStored(session, accountId, detailedEmail); + log('StoreNewEmailInteractor::execute():isNewEmailExist: $isNewEmailExist'); + if (!isNewEmailExist) { + await Future.wait([ + _emailRepository.storeEmail(session, accountId, email), + _emailRepository.storeDetailedNewEmail(session, accountId, detailedEmail), + ], eagerError: true); + } + } +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/store_opened_email_interactor.dart b/lib/features/email/domain/usecases/store_opened_email_interactor.dart new file mode 100644 index 0000000000..50897fbdb6 --- /dev/null +++ b/lib/features/email/domain/usecases/store_opened_email_interactor.dart @@ -0,0 +1,46 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_cache_exceptions.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/store_opened_email_state.dart'; + +class StoreOpenedEmailInteractor { + final EmailRepository _emailRepository; + + StoreOpenedEmailInteractor(this._emailRepository); + + Stream> execute( + Session session, + AccountId accountId, + DetailedEmail detailedEmail + ) async* { + try { + yield Right(StoreOpenedEmailLoading()); + final isOpenedEmailExist = await _isOpenedEmailAlreadyStored(session, accountId, detailedEmail); + log('StoreOpenedEmailInteractor::execute():isOpenedEmailExist: $isOpenedEmailExist'); + if (!isOpenedEmailExist) { + await _emailRepository.storeOpenedEmail(session, accountId, detailedEmail); + yield Right(StoreOpenedEmailSuccess()); + } else { + yield Left(StoreOpenedEmailFailure(OpenedEmailAlreadyStoredException())); + } + } catch (e) { + yield Left(StoreOpenedEmailFailure(e)); + } + } + + Future _isOpenedEmailAlreadyStored(Session session, AccountId accountId, DetailedEmail detailedEmail) async { + try { + await _emailRepository.getStoredOpenedEmail(session, accountId, detailedEmail.emailId); + return true; + } catch (err) { + logError('StoreOpenedEmailInteractor::isOpenedEmailAlreadyStored():EXCEPTION: $err'); + return false; + } + } +} \ No newline at end of file diff --git a/lib/features/email/domain/usecases/transform_html_email_content_interactor.dart b/lib/features/email/domain/usecases/transform_html_email_content_interactor.dart new file mode 100644 index 0000000000..00b18c8931 --- /dev/null +++ b/lib/features/email/domain/usecases/transform_html_email_content_interactor.dart @@ -0,0 +1,26 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/state/transform_html_email_content_state.dart'; + +class TransformHtmlEmailContentInteractor { + final EmailRepository emailRepository; + + TransformHtmlEmailContentInteractor(this.emailRepository); + + Stream> execute( + String htmlContent, + TransformConfiguration configuration + ) async* { + try { + yield Right(TransformHtmlEmailContentLoading()); + final htmlContentTransformed = await emailRepository.transformHtmlEmailContent(htmlContent, configuration); + yield Right(TransformHtmlEmailContentSuccess(htmlContentTransformed)); + } catch (e) { + yield Left(TransformHtmlEmailContentFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/action/email_ui_action.dart b/lib/features/email/presentation/action/email_ui_action.dart index eeb63d8ae0..3d8ee6ae51 100644 --- a/lib/features/email/presentation/action/email_ui_action.dart +++ b/lib/features/email/presentation/action/email_ui_action.dart @@ -18,4 +18,6 @@ class RefreshChangeEmailAction extends EmailUIAction { @override List get props => [newState]; -} \ No newline at end of file +} + +class CloseEmailDetailedViewAction extends EmailUIAction {} \ No newline at end of file diff --git a/lib/features/email/presentation/bindings/calendar_event_interactor_bindings.dart b/lib/features/email/presentation/bindings/calendar_event_interactor_bindings.dart new file mode 100644 index 0000000000..fdf4369a21 --- /dev/null +++ b/lib/features/email/presentation/bindings/calendar_event_interactor_bindings.dart @@ -0,0 +1,53 @@ +import 'package:core/data/model/source_type/data_source_type.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/http/http_client.dart'; +import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/calendar_event_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/calendar_event_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/local_calendar_event_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; +import 'package:tmail_ui_user/features/email/data/network/calendar_event_api.dart'; +import 'package:tmail_ui_user/features/email/data/repository/calendar_event_repository_impl.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/calendar_event_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/parse_calendar_event_interactor.dart'; +import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; + +class CalendarEventInteractorBindings extends InteractorsBindings { + + @override + void bindingsDataSource() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsDataSourceImpl() { + Get.lazyPut(() => CalendarEventAPI(Get.find())); + Get.lazyPut(() => CalendarEventDataSourceImpl( + Get.find(), + Get.find())); + Get.lazyPut(() => LocalCalendarEventDataSourceImpl( + Get.find(), + Get.find())); + } + + @override + void bindingsInteractor() { + Get.lazyPut(() => ParseCalendarEventInteractor(Get.find())); + } + + @override + void bindingsRepository() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsRepositoryImpl() { + Get.lazyPut(() => CalendarEventRepositoryImpl( + { + DataSourceType.network: Get.find(), + DataSourceType.local: Get.find(), + } + )); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/email_bindings.dart b/lib/features/email/presentation/bindings/email_bindings.dart similarity index 71% rename from lib/features/email/presentation/email_bindings.dart rename to lib/features/email/presentation/bindings/email_bindings.dart index 03c3db6b1c..a2563000e8 100644 --- a/lib/features/email/presentation/email_bindings.dart +++ b/lib/features/email/presentation/bindings/email_bindings.dart @@ -1,10 +1,12 @@ import 'package:core/core.dart'; +import 'package:core/utils/file_utils.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; @@ -16,22 +18,12 @@ import 'package:tmail_ui_user/features/email/domain/usecases/export_attachment_i import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_stored_email_state_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart'; -import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_interactor.dart'; -import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/account_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/hive_account_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; +import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; -import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; -import 'package:tmail_ui_user/features/login/data/repository/account_repository_impl.dart'; -import 'package:tmail_ui_user/features/login/data/repository/authentication_oidc_repository_impl.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; @@ -47,6 +39,12 @@ import 'package:tmail_ui_user/features/mailbox/data/repository/mailbox_repositor import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identity_interactors_bindings.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; @@ -56,15 +54,16 @@ class EmailBindings extends BaseBindings { void bindingsController() { Get.put(EmailSupervisorController()); Get.put(SingleEmailController( - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find() )); } @@ -74,8 +73,6 @@ class EmailBindings extends BaseBindings { Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); } @override @@ -90,21 +87,19 @@ class EmailBindings extends BaseBindings { Get.lazyPut(() => EmailDataSourceImpl( Get.find(), Get.find())); - Get.lazyPut(() => HiveAccountDatasourceImpl( - Get.find(), - Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find(), Get.find())); Get.lazyPut(() => StateDataSourceImpl(Get.find(), Get.find())); - Get.lazyPut(() => AuthenticationOIDCDataSourceImpl( - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - )); + Get.lazyPut(() => EmailHiveCacheDataSourceImpl( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find())); } @override @@ -136,6 +131,7 @@ class EmailBindings extends BaseBindings { Get.find(), Get.find())); Get.lazyPut(() => GetStoredEmailStateInteractor(Get.find())); + Get.lazyPut(() => StoreOpenedEmailInteractor(Get.find())); IdentityInteractorsBindings().dependencies(); } @@ -143,8 +139,6 @@ class EmailBindings extends BaseBindings { void bindingsRepository() { Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); } @override @@ -157,11 +151,12 @@ class EmailBindings extends BaseBindings { Get.find(), )); Get.lazyPut(() => EmailRepositoryImpl( - Get.find(), - Get.find(), - Get.find() + { + DataSourceType.network: Get.find(), + DataSourceType.hiveCache: Get.find() + }, + Get.find(), + Get.find() )); - Get.lazyPut(() => AccountRepositoryImpl(Get.find())); - Get.lazyPut(() => AuthenticationOIDCRepositoryImpl(Get.find())); } } \ No newline at end of file diff --git a/lib/features/email/presentation/mdn_interactor_bindings.dart b/lib/features/email/presentation/bindings/mdn_interactor_bindings.dart similarity index 100% rename from lib/features/email/presentation/mdn_interactor_bindings.dart rename to lib/features/email/presentation/bindings/mdn_interactor_bindings.dart diff --git a/lib/features/email/presentation/controller/email_supervisor_controller.dart b/lib/features/email/presentation/controller/email_supervisor_controller.dart index 39f7070008..00879c3128 100644 --- a/lib/features/email/presentation/controller/email_supervisor_controller.dart +++ b/lib/features/email/presentation/controller/email_supervisor_controller.dart @@ -1,6 +1,6 @@ import 'dart:collection'; import 'package:collection/collection.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -55,7 +55,7 @@ class EmailSupervisorController extends GetxController { bool get isSearchActivatedOnMobile { return mailboxDashBoardController.searchController.isSearchEmailRunning - && !BuildUtils.isWeb; + && PlatformInfo.isMobile; } void createPageControllerAndJumpToEmailById(EmailId currentEmailId) { @@ -112,7 +112,7 @@ class EmailSupervisorController extends GetxController { } void _jumpToPage(int page) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { pageController?.jumpToPage(page); } else { pageController?.animateToPage( @@ -123,7 +123,7 @@ class EmailSupervisorController extends GetxController { } void updateScrollPhysicPageView({bool isScrollPageViewActivated = false}) { - if (BuildUtils.isWeb || !isScrollPageViewActivated) { + if (PlatformInfo.isWeb || !isScrollPageViewActivated) { scrollPhysicsPageView.value = const NeverScrollableScrollPhysics(); } else { scrollPhysicsPageView.value = null; diff --git a/lib/features/email/presentation/controller/single_email_controller.dart b/lib/features/email/presentation/controller/single_email_controller.dart index 2bd2048168..a68bdc74fa 100644 --- a/lib/features/email/presentation/controller/single_email_controller.dart +++ b/lib/features/email/presentation/controller/single_email_controller.dart @@ -6,13 +6,14 @@ import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mdn/disposition.dart'; @@ -21,11 +22,20 @@ import 'package:model/model.dart'; import 'package:better_open_file/better_open_file.dart' as open_file; import 'package:permission_handler/permission_handler.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:share/share.dart' as share_library; +import 'package:share_plus/share_plus.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; +import 'package:tmail_ui_user/features/email/domain/extensions/list_attachments_extension.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; +import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; +import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/parse_calendar_event_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/store_opened_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; +import 'package:tmail_ui_user/features/email/presentation/bindings/calendar_event_interactor_bindings.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/model/email_loaded.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; @@ -64,11 +74,14 @@ import 'package:tmail_ui_user/features/manage_account/presentation/extensions/da import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/navigation_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; import 'package:uuid/uuid.dart'; class SingleEmailController extends BaseController with AppLoaderMixin { @@ -80,6 +93,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final _appToast = Get.find(); final _uuid = Get.find(); final _downloadManager = Get.find(); + final _dynamicUrlInterceptors = Get.find(); final GetEmailContentInteractor _getEmailContentInteractor; final MarkAsEmailReadInteractor _markAsEmailReadInteractor; @@ -90,17 +104,21 @@ class SingleEmailController extends BaseController with AppLoaderMixin { final MarkAsStarEmailInteractor _markAsStarEmailInteractor; final DownloadAttachmentForWebInteractor _downloadAttachmentForWebInteractor; final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; + final StoreOpenedEmailInteractor _storeOpenedEmailInteractor; CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; SendReceiptToSenderInteractor? _sendReceiptToSenderInteractor; + ParseCalendarEventInteractor? _parseCalendarEventInteractor; - final emailAddressExpandMode = ExpandMode.COLLAPSE.obs; - final attachmentsExpandMode = ExpandMode.COLLAPSE.obs; - final emailContents = [].obs; + final emailContents = RxnString(); final attachments = [].obs; + final calendarEvent = Rxn(); + final eventActions = [].obs; + final emailLoadedViewState = Rx>(Right(UIState.idle)); + EmailId? _currentEmailId; Identity? _identitySelected; - List? initialEmailContents; + EmailLoaded? _currentEmailLoaded; final StreamController> _downloadProgressStateController = StreamController>.broadcast(); @@ -108,10 +126,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { PresentationEmail? get currentEmail => mailboxDashBoardController.selectedEmail.value; - bool get isDisplayFullEmailAddress => emailAddressExpandMode.value == ExpandMode.EXPAND; - - bool get isDisplayFullAttachments => attachmentsExpandMode.value == ExpandMode.EXPAND; - SingleEmailController( this._getEmailContentInteractor, this._markAsEmailReadInteractor, @@ -122,6 +136,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { this._markAsStarEmailInteractor, this._downloadAttachmentForWebInteractor, this._getAllIdentitiesInteractor, + this._storeOpenedEmailInteractor ); @override @@ -137,6 +152,54 @@ class SingleEmailController extends BaseController with AppLoaderMixin { super.onClose(); } + @override + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetEmailContentSuccess) { + _getEmailContentSuccess(success); + } else if (success is GetEmailContentFromCacheSuccess) { + _getEmailContentOfflineSuccess(success); + } else if (success is MarkAsEmailReadSuccess) { + _markAsEmailReadSuccess(success); + } else if (success is ExportAttachmentSuccess) { + _exportAttachmentSuccessAction(success); + } else if (success is MoveToMailboxSuccess) { + _moveToMailboxSuccess(success); + } else if (success is MarkAsStarEmailSuccess) { + _markAsEmailStarSuccess(success); + } else if (success is DownloadAttachmentForWebSuccess) { + _downloadAttachmentForWebSuccessAction(success); + } else if (success is GetAllIdentitiesSuccess) { + _getAllIdentitiesSuccess(success); + } else if (success is SendReceiptToSenderSuccess) { + _sendReceiptToSenderSuccess(success); + } else if (success is CreateNewRuleFilterSuccess) { + _createNewRuleFilterSuccess(success); + } else if (success is ParseCalendarEventLoading) { + emailLoadedViewState.value = Right(success); + } else if (success is ParseCalendarEventSuccess) { + _handleParseCalendarEventSuccess(success); + } + } + + @override + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is MarkAsEmailReadFailure) { + _markAsEmailReadFailure(failure); + } else if (failure is DownloadAttachmentsFailure) { + _downloadAttachmentsFailure(failure); + } else if (failure is ExportAttachmentFailure) { + _exportAttachmentFailureAction(failure); + } else if (failure is DownloadAttachmentForWebFailure) { + _downloadAttachmentForWebFailureAction(failure); + } else if (failure is ParseCalendarEventFailure) { + _handleParseCalendarEventFailure(failure); + } else if (failure is GetEmailContentFailure) { + emailLoadedViewState.value = Left(failure); + } + } + void _registerObxStreamListener() { ever(mailboxDashBoardController.accountId, (accountId) { if (accountId is AccountId) { @@ -151,6 +214,21 @@ class SingleEmailController extends BaseController with AppLoaderMixin { mailboxDashBoardController.selectedEmail, _handleOpenEmailDetailedView ); + + ever(mailboxDashBoardController.emailUIAction, (action) { + if (action is CloseEmailDetailedViewAction) { + if (emailSupervisorController.supportedPageView.isTrue) { + emailSupervisorController.popEmailQueue(_currentEmailId); + emailSupervisorController.setCurrentEmailIndex(-1); + emailSupervisorController.disposePageViewController(); + } + _updateCurrentEmailId(null); + _resetToOriginalValue(); + mailboxDashBoardController.clearSelectedEmail(); + mailboxDashBoardController.dispatchRoute(DashboardRoutes.thread); + mailboxDashBoardController.clearEmailUIAction(); + } + }); } bool isListEmailContainSelectedEmail(PresentationEmail selectedEmail) { @@ -163,19 +241,20 @@ class SingleEmailController extends BaseController with AppLoaderMixin { log('SingleEmailController::_handleOpenEmailDetailedView(): email unselected'); return; } + emailLoadedViewState.value = Right(GetEmailContentLoading()); emailSupervisorController.updateNewCurrentListEmail(); _updateCurrentEmailId(selectedEmail.id); _resetToOriginalValue(); if (isListEmailContainSelectedEmail(selectedEmail)) { - _createMultipleEmailViewAsPageView(selectedEmail.id); + _createMultipleEmailViewAsPageView(selectedEmail.id!); } else { - _createSingleEmailView(selectedEmail.id); + _createSingleEmailView(selectedEmail.id!); } if (!selectedEmail.hasRead) { - markAsEmailRead(selectedEmail, ReadActions.markAsRead); + markAsEmailRead(selectedEmail, ReadActions.markAsRead, MarkReadAction.tap); } if (_identitySelected == null) { @@ -212,11 +291,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { taskId: success.taskId, attachment: success.attachment)); - if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon(currentOverlayContext!, - message: AppLocalizations.of(currentContext!).your_download_has_started, - iconColor: AppColor.primaryColor, - icon: imagePaths.icDownload); + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).your_download_has_started, + leadingSVGIconColor: AppColor.primaryColor, + leadingSVGIcon: imagePaths.icDownload); } } else if (success is DownloadingAttachmentForWeb) { final percent = success.progress.round(); @@ -240,19 +320,26 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _injectAndGetInteractorBindings(Session? session, AccountId accountId) { injectRuleFilterBindings(session, accountId); injectMdnBindings(session, accountId); + _injectCalendarEventBindings(session, accountId); - if (Get.isRegistered()) { - _createNewEmailRuleFilterInteractor = Get.find(); - } - if (Get.isRegistered()) { - _sendReceiptToSenderInteractor = Get.find(); + _createNewEmailRuleFilterInteractor = getBinding(); + _sendReceiptToSenderInteractor = getBinding(); + _parseCalendarEventInteractor = getBinding(); + } + + void _injectCalendarEventBindings(Session? session, AccountId? accountId) { + if (session != null && accountId != null) { + if (CapabilityIdentifier.jamesCalendarEvent.isSupported(session, accountId)) { + CalendarEventInteractorBindings().dependencies(); + } } } void _getAllIdentities() { final accountId = mailboxDashBoardController.accountId.value; - if (accountId != null) { - consumeState(_getAllIdentitiesInteractor.execute(accountId)); + final session = mailboxDashBoardController.sessionCurrent; + if (accountId != null && session != null) { + consumeState(_getAllIdentitiesInteractor.execute(session, accountId)); } } @@ -290,78 +377,116 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void _getEmailContentAction(EmailId emailId) async { - final accountId = mailboxDashBoardController.accountId.value; - final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(); final emailLoaded = emailSupervisorController.getEmailInQueueByEmailId(emailId); if (emailLoaded != null) { - dispatchState(Right(GetEmailContentLoading())); - await Future.delayed(const Duration(milliseconds: 300)); - consumeState(Stream.value(Right(GetEmailContentSuccess( - emailLoaded.emailContents, - emailLoaded.emailContentsDisplayed, - emailLoaded.attachments, - emailLoaded.emailCurrent)))); - } else if (accountId != null && baseDownloadUrl != null) { - consumeState(_getEmailContentInteractor.execute(accountId, emailId, baseDownloadUrl)); + consumeState(Stream.value(Right( + GetEmailContentSuccess( + htmlEmailContent: emailLoaded.htmlContent, + attachments: emailLoaded.attachments, + emailCurrent: emailLoaded.emailCurrent + ) + ))); + } else { + final session = mailboxDashBoardController.sessionCurrent; + final accountId = mailboxDashBoardController.accountId.value; + if (session != null && accountId != null) { + final baseDownloadUrl = mailboxDashBoardController.sessionCurrent?.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl) ?? ''; + TransformConfiguration transformConfiguration = PlatformInfo.isWeb + ? TransformConfiguration.forPreviewEmailOnWeb() + : TransformConfiguration.forPreviewEmail(); + + consumeState(_getEmailContentInteractor.execute( + session, + accountId, + emailId, + baseDownloadUrl, + transformConfiguration + )); + } } } - @override - void onDone() { - viewState.value.fold( - (failure) { - if (failure is MarkAsEmailReadFailure) { - _markAsEmailReadFailure(failure); - } else if (failure is DownloadAttachmentsFailure) { - _downloadAttachmentsFailure(failure); - } else if (failure is ExportAttachmentFailure) { - _exportAttachmentFailureAction(failure); - } else if (failure is DownloadAttachmentForWebFailure) { - _downloadAttachmentForWebFailureAction(failure); - } - }, - (success) { - if (success is GetEmailContentSuccess) { - _getEmailContentSuccess(success); - } else if (success is MarkAsEmailReadSuccess) { - _markAsEmailReadSuccess(success); - } else if (success is ExportAttachmentSuccess) { - _exportAttachmentSuccessAction(success); - } else if (success is MoveToMailboxSuccess) { - _moveToMailboxSuccess(success); - } else if (success is MarkAsStarEmailSuccess) { - _markAsEmailStarSuccess(success); - } else if (success is DownloadAttachmentForWebSuccess) { - _downloadAttachmentForWebSuccessAction(success); - } else if (success is GetAllIdentitiesSuccess) { - _getAllIdentitiesSuccess(success); - } else if (success is SendReceiptToSenderSuccess) { - _sendReceiptToSenderSuccess(success); - } else if (success is CreateNewRuleFilterSuccess) { - _createNewRuleFilterSuccess(success); - } - }); + void _getEmailContentOfflineSuccess(GetEmailContentFromCacheSuccess success) { + emailLoadedViewState.value = Right(success); + if (emailSupervisorController.presentationEmailsLoaded.length > ThreadConstants.defaultLimit.value.toInt()) { + emailSupervisorController.popFirstEmailQueue(); + } + emailSupervisorController.popEmailQueue(success.emailCurrent?.id); + + _currentEmailLoaded = EmailLoaded( + htmlContent: success.htmlEmailContent, + attachments: List.of(success.attachments), + emailCurrent: success.emailCurrent, + ); + emailSupervisorController.pushEmailQueue(_currentEmailLoaded!); + + if (success.emailCurrent?.id == currentEmail?.id) { + attachments.value = success.attachments; + + if (_canParseCalendarEvent(blobIds: success.attachments.calendarEventBlobIds)) { + _parseCalendarEventAction( + accountId: mailboxDashBoardController.accountId.value!, + blobIds: success.attachments.calendarEventBlobIds, + emailContents: success.htmlEmailContent + ); + } else { + emailContents.value = success.htmlEmailContent; + } + + final isShowMessageReadReceipt = success.emailCurrent?.hasReadReceipt(mailboxDashBoardController.mapMailboxById) == true; + if (isShowMessageReadReceipt) { + _handleReadReceipt(); + } + } } void _getEmailContentSuccess(GetEmailContentSuccess success) { - if(emailSupervisorController.presentationEmailsLoaded.length > ThreadConstants.defaultLimit.value.toInt()) { + emailLoadedViewState.value = Right(success); + if (emailSupervisorController.presentationEmailsLoaded.length > ThreadConstants.defaultLimit.value.toInt()) { emailSupervisorController.popFirstEmailQueue(); } emailSupervisorController.popEmailQueue(success.emailCurrent?.id); - emailSupervisorController.pushEmailQueue(EmailLoaded( - success.emailContents.toList(), - success.emailContentsDisplayed.toList(), - success.attachments.toList(), - success.emailCurrent, - )); + _currentEmailLoaded = EmailLoaded( + htmlContent: success.htmlEmailContent, + attachments: List.of(success.attachments), + emailCurrent: success.emailCurrent, + ); + emailSupervisorController.pushEmailQueue(_currentEmailLoaded!); if (success.emailCurrent?.id == currentEmail?.id) { - emailContents.value = success.emailContentsDisplayed; - initialEmailContents = success.emailContents; attachments.value = success.attachments; + if (_canParseCalendarEvent(blobIds: success.attachments.calendarEventBlobIds)) { + _parseCalendarEventAction( + accountId: mailboxDashBoardController.accountId.value!, + blobIds: success.attachments.calendarEventBlobIds, + emailContents: success.htmlEmailContent + ); + } else { + emailContents.value = success.htmlEmailContent; + } + + if (PlatformInfo.isMobile) { + final detailedEmail = DetailedEmail( + emailId: currentEmail!.id!, + createdTime: currentEmail?.receivedAt?.value ?? DateTime.now(), + attachments: success.attachments, + headers: currentEmail?.emailHeader?.toSet(), + keywords: currentEmail?.keywords, + htmlEmailContent: success.htmlEmailContent, + messageId: success.emailCurrent?.messageId, + references: success.emailCurrent?.references, + ); + + _storeOpenedEmailAction( + mailboxDashBoardController.sessionCurrent, + mailboxDashBoardController.accountId.value, + detailedEmail + ); + } + final isShowMessageReadReceipt = success.emailCurrent ?.hasReadReceipt(mailboxDashBoardController.mapMailboxById) == true; if (isShowMessageReadReceipt) { @@ -385,11 +510,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void _resetToOriginalValue() { - attachmentsExpandMode.value = ExpandMode.COLLAPSE; - emailAddressExpandMode.value = ExpandMode.COLLAPSE; - emailContents.clear(); - initialEmailContents?.clear(); + emailContents.value = null; + _currentEmailLoaded = null; attachments.clear(); + calendarEvent.value = null; + eventActions.clear(); } PresentationMailbox? getMailboxContain(PresentationEmail email) { @@ -402,10 +527,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void markAsEmailRead(PresentationEmail presentationEmail, ReadActions readActions) async { + void markAsEmailRead(PresentationEmail presentationEmail, ReadActions readActions, MarkReadAction markReadAction) async { final accountId = mailboxDashBoardController.accountId.value; - if (accountId != null) { - consumeState(_markAsEmailReadInteractor.execute(accountId, presentationEmail.toEmail(), readActions)); + final session = mailboxDashBoardController.sessionCurrent; + if (accountId != null && session != null) { + consumeState(_markAsEmailReadInteractor.execute(session, accountId, presentationEmail.toEmail(), readActions, markReadAction)); } } @@ -428,13 +554,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void toggleDisplayAttachmentsAction() { - final newExpandMode = attachmentsExpandMode.value == ExpandMode.COLLAPSE - ? ExpandMode.EXPAND - : ExpandMode.COLLAPSE; - attachmentsExpandMode.value = newExpandMode; - } - void downloadAttachments(BuildContext context, List attachments) async { final needRequestPermission = await _deviceManager.isNeedRequestStoragePermissionOnAndroid(); @@ -445,7 +564,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _downloadAttachmentsAction(attachments); break; case PermissionStatus.permanentlyDenied: - _appToast.showToast(AppLocalizations.of(context).you_need_to_grant_files_permission_to_download_attachments); + if (context.mounted && currentOverlayContext != null && currentContext != null) { + _appToast.showToastMessage( + currentOverlayContext!, + AppLocalizations.of(context).you_need_to_grant_files_permission_to_download_attachments, + ); + } break; default: { final requested = await Permission.storage.request(); @@ -454,7 +578,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { _downloadAttachmentsAction(attachments); break; default: - _appToast.showToast(AppLocalizations.of(context).you_need_to_grant_files_permission_to_download_attachments); + if (context.mounted && currentOverlayContext != null && currentContext != null) { + _appToast.showToastMessage( + currentOverlayContext!, + AppLocalizations.of(context).you_need_to_grant_files_permission_to_download_attachments, + ); + } break; } } @@ -467,14 +596,16 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _downloadAttachmentsAction(List attachments) async { final accountId = mailboxDashBoardController.accountId.value; if (accountId != null && mailboxDashBoardController.sessionCurrent != null) { - final baseDownloadUrl = mailboxDashBoardController.sessionCurrent!.getDownloadUrl(); + final baseDownloadUrl = mailboxDashBoardController.sessionCurrent!.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl); consumeState(_downloadAttachmentsInteractor.execute(attachments, accountId, baseDownloadUrl)); } } void _downloadAttachmentsFailure(DownloadAttachmentsFailure failure) { - if (currentContext != null) { - _appToast.showErrorToast(AppLocalizations.of(currentContext!).attachment_download_failed); + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).attachment_download_failed); } } @@ -514,7 +645,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _exportAttachmentAction(Attachment attachment, CancelToken cancelToken) async { final accountId = mailboxDashBoardController.accountId.value; if (accountId != null && mailboxDashBoardController.sessionCurrent != null) { - final baseDownloadUrl = mailboxDashBoardController.sessionCurrent!.getDownloadUrl(); + final baseDownloadUrl = mailboxDashBoardController.sessionCurrent!.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl); consumeState(_exportAttachmentInteractor.execute(attachment, accountId, baseDownloadUrl, cancelToken)); } } @@ -522,8 +653,11 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _exportAttachmentFailureAction(ExportAttachmentFailure failure) { if (failure.exception is! CancelDownloadFileException) { popBack(); - if (currentContext != null) { - _appToast.showErrorToast(AppLocalizations.of(currentContext!).attachment_download_failed); + + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).attachment_download_failed); } } } @@ -536,7 +670,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _openDownloadedPreviewWorkGroupDocument(DownloadedResponse downloadedResponse) async { log('SingleEmailController::_openDownloadedPreviewWorkGroupDocument(): $downloadedResponse'); if (downloadedResponse.mediaType == null) { - await share_library.Share.shareFiles([downloadedResponse.filePath]); + await Share.shareXFiles([XFile(downloadedResponse.filePath)]); } final openResult = await open_file.OpenFile.open( @@ -546,8 +680,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { if (openResult.type != open_file.ResultType.done) { logError('SingleEmailController::_openDownloadedPreviewWorkGroupDocument(): no preview available'); - if (currentContext != null) { - _appToast.showErrorToast(AppLocalizations.of(currentContext!).noPreviewAvailable); + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).noPreviewAvailable); } } } @@ -559,7 +695,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _downloadAttachmentForWebAction(BuildContext context, Attachment attachment) async { final accountId = mailboxDashBoardController.accountId.value; if (accountId != null && mailboxDashBoardController.sessionCurrent != null) { - final baseDownloadUrl = mailboxDashBoardController.sessionCurrent!.getDownloadUrl(); + final baseDownloadUrl = mailboxDashBoardController.sessionCurrent!.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors.jmapUrl); final generateTaskId = DownloadTaskId(_uuid.v4()); consumeState(_downloadAttachmentForWebInteractor.execute( generateTaskId, @@ -583,54 +719,42 @@ class SingleEmailController extends BaseController with AppLoaderMixin { log('SingleEmailController::_downloadAttachmentForWebFailureAction(): $failure'); mailboxDashBoardController.deleteDownloadTask(failure.taskId); - if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon(currentOverlayContext!, - message: AppLocalizations.of(currentContext!).attachment_download_failed, - bgColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - iconColor: Colors.white, - icon: imagePaths.icDownload); + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).attachment_download_failed); } } void moveToMailbox(BuildContext context, PresentationEmail email) async { final currentMailbox = getMailboxContain(email); final accountId = mailboxDashBoardController.accountId.value; - final _session = mailboxDashBoardController.sessionCurrent; + final session = mailboxDashBoardController.sessionCurrent; if (currentMailbox != null && accountId != null) { - final arguments = DestinationPickerArguments(accountId, MailboxActions.moveEmail, _session); - if (BuildUtils.isWeb) { - showDialogDestinationPicker( - context: context, - arguments: arguments, - onSelectedMailbox: (destinationMailbox) { - if (mailboxDashBoardController.sessionCurrent != null) { - _dispatchMoveToAction( - context, - accountId, - mailboxDashBoardController.sessionCurrent!, - email, - currentMailbox, - destinationMailbox); - } - }); - } else { - final destinationMailbox = await push( - AppRoutes.destinationPicker, - arguments: arguments); - - if (destinationMailbox != null && - destinationMailbox is PresentationMailbox && - mailboxDashBoardController.sessionCurrent != null) { - _dispatchMoveToAction( - context, - accountId, - mailboxDashBoardController.sessionCurrent!, - email, - currentMailbox, - destinationMailbox); - } + final arguments = DestinationPickerArguments( + accountId, + MailboxActions.moveEmail, + session, + mailboxIdSelected: currentMailbox.mailboxId + ); + + final destinationMailbox = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments) + : await push(AppRoutes.destinationPicker, arguments: arguments); + + if (destinationMailbox != null && + destinationMailbox is PresentationMailbox && + mailboxDashBoardController.sessionCurrent != null && + context.mounted + ) { + _dispatchMoveToAction( + context, + accountId, + mailboxDashBoardController.sessionCurrent!, + email, + currentMailbox, + destinationMailbox); } } } @@ -644,22 +768,34 @@ class SingleEmailController extends BaseController with AppLoaderMixin { PresentationMailbox destinationMailbox ) { if (destinationMailbox.isTrash) { - _moveToTrashAction(context, accountId, MoveToMailboxRequest( - {currentMailbox.id: [emailSelected.id]}, + _moveToTrashAction( + context, + session, + accountId, + MoveToMailboxRequest( + {currentMailbox.id: [emailSelected.id!]}, destinationMailbox.id, MoveAction.moving, session, EmailActionType.moveToTrash)); } else if (destinationMailbox.isSpam) { - _moveToSpamAction(context, accountId, MoveToMailboxRequest( - {currentMailbox.id: [emailSelected.id]}, + _moveToSpamAction( + context, + session, + accountId, + MoveToMailboxRequest( + {currentMailbox.id: [emailSelected.id!]}, destinationMailbox.id, MoveAction.moving, session, EmailActionType.moveToSpam)); } else { - _moveToMailbox(context, accountId, MoveToMailboxRequest( - {currentMailbox.id: [emailSelected.id]}, + _moveToMailbox( + context, + session, + accountId, + MoveToMailboxRequest( + {currentMailbox.id: [emailSelected.id!]}, destinationMailbox.id, MoveAction.moving, session, @@ -668,15 +804,15 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void _moveToMailbox(BuildContext context, AccountId accountId, MoveToMailboxRequest moveRequest) { + void _moveToMailbox(BuildContext context, Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { closeEmailView(context); - consumeState(_moveToMailboxInteractor.execute(accountId, moveRequest)); + consumeState(_moveToMailboxInteractor.execute(session, accountId, moveRequest)); } void _moveToMailboxSuccess(MoveToMailboxSuccess success) { mailboxDashBoardController.dispatchState(Right(success)); if (success.moveAction == MoveAction.moving && currentContext != null && currentOverlayContext != null) { - _appToast.showBottomToast( + _appToast.showToastMessage( currentOverlayContext!, success.emailActionType.getToastMessageMoveToMailboxSuccess(currentContext!, destinationPath: success.destinationPath), actionName: AppLocalizations.of(currentContext!).undo, @@ -688,57 +824,67 @@ class SingleEmailController extends BaseController with AppLoaderMixin { mailboxDashBoardController.sessionCurrent!, success.emailActionType)); }, - leadingIcon: SvgPicture.asset( - imagePaths.icFolderMailbox, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), + leadingSVGIcon: imagePaths.icFolderMailbox, + leadingSVGIconColor: Colors.white, backgroundColor: AppColor.toastSuccessBackgroundColor, textColor: Colors.white, - textActionColor: Colors.white, - actionIcon: SvgPicture.asset(imagePaths.icUndo), - maxWidth: responsiveUtils.getMaxWidthToast(currentContext!) + actionIcon: SvgPicture.asset(imagePaths.icUndo) ); } } void _revertedToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { final accountId = mailboxDashBoardController.accountId.value; - if (accountId != null) { - _moveToMailbox(currentContext!, accountId, newMoveRequest); + final session = mailboxDashBoardController.sessionCurrent; + if (accountId != null && session != null) { + _moveToMailbox(currentContext!, session, accountId, newMoveRequest); } } void moveToTrash(BuildContext context, PresentationEmail email) async { + final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; final trashMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleTrash); final currentMailbox = getMailboxContain(email); - if (accountId != null && currentMailbox != null && trashMailboxId != null) { - _moveToTrashAction(context, accountId, MoveToMailboxRequest( - {currentMailbox.id: [email.id]}, - trashMailboxId, - MoveAction.moving, - mailboxDashBoardController.sessionCurrent!, - EmailActionType.moveToTrash) + if (session != null && accountId != null && currentMailbox != null && trashMailboxId != null) { + _moveToTrashAction( + context, + session, + accountId, + MoveToMailboxRequest( + {currentMailbox.id: [email.id!]}, + trashMailboxId, + MoveAction.moving, + mailboxDashBoardController.sessionCurrent!, + EmailActionType.moveToTrash) ); } } - void _moveToTrashAction(BuildContext context, AccountId accountId, MoveToMailboxRequest moveRequest) { + void _moveToTrashAction( + BuildContext context, + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest + ) { closeEmailView(context); - mailboxDashBoardController.moveToMailbox(accountId, moveRequest); + mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); } void moveToSpam(BuildContext context, PresentationEmail email) async { + final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; final spamMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleSpam); final currentMailbox = getMailboxContain(email); - if (accountId != null && currentMailbox != null && spamMailboxId != null) { - _moveToSpamAction(context, accountId, MoveToMailboxRequest( - {currentMailbox.id: [email.id]}, + if (session != null && accountId != null && currentMailbox != null && spamMailboxId != null) { + _moveToSpamAction( + context, + session, + accountId, + MoveToMailboxRequest( + {currentMailbox.id: [email.id!]}, spamMailboxId, MoveAction.moving, mailboxDashBoardController.sessionCurrent!, @@ -748,13 +894,18 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } void unSpam(BuildContext context, PresentationEmail email) async { + final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; final spamMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleSpam); final inboxMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleInbox); - if (accountId != null && spamMailboxId != null && inboxMailboxId != null) { - _moveToSpamAction(context, accountId, MoveToMailboxRequest( - {spamMailboxId: [email.id]}, + if (session != null && accountId != null && spamMailboxId != null && inboxMailboxId != null) { + _moveToSpamAction( + context, + session, + accountId, + MoveToMailboxRequest( + {spamMailboxId: [email.id!]}, inboxMailboxId, MoveAction.moving, mailboxDashBoardController.sessionCurrent!, @@ -763,15 +914,21 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void _moveToSpamAction(BuildContext context, AccountId accountId, MoveToMailboxRequest moveRequest) { + void _moveToSpamAction( + BuildContext context, + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest + ) { closeEmailView(context); - mailboxDashBoardController.moveToMailbox(accountId, moveRequest); + mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); } void markAsStarEmail(PresentationEmail presentationEmail, MarkStarAction markStarAction) async { final accountId = mailboxDashBoardController.accountId.value; - if (accountId != null) { - consumeState(_markAsStarEmailInteractor.execute(accountId, presentationEmail.toEmail(), markStarAction)); + final session = mailboxDashBoardController.sessionCurrent; + if (accountId != null && session != null) { + consumeState(_markAsStarEmailInteractor.execute(session, accountId, presentationEmail.toEmail(), markStarAction)); } } @@ -787,7 +944,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { switch(actionType) { case EmailActionType.markAsUnread: popBack(); - markAsEmailRead(presentationEmail, ReadActions.markAsUnread); + markAsEmailRead(presentationEmail, ReadActions.markAsUnread, MarkReadAction.tap); break; case EmailActionType.markAsStarred: markAsStarEmail(presentationEmail, MarkStarAction.markStar); @@ -817,14 +974,6 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void expandEmailAddress() { - emailAddressExpandMode.value = ExpandMode.EXPAND; - } - - void collapseEmailAddress() { - emailAddressExpandMode.value = ExpandMode.COLLAPSE; - } - void openEmailAddressDialog(BuildContext context, EmailAddress emailAddress) { if (responsiveUtils.isScreenWithShortestSide(context)) { (EmailAddressBottomSheetBuilder(context, imagePaths, emailAddress) @@ -852,23 +1001,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void copyEmailAddress(BuildContext context, EmailAddress emailAddress) { popBack(); - - Clipboard.setData(ClipboardData(text: emailAddress.emailAddress)).then((_){ - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(AppLocalizations.of(context).email_address_copied_to_clipboard)) - ); - }); + AppUtils.copyEmailAddressToClipboard(context, emailAddress.emailAddress); } void composeEmailFromEmailAddress(EmailAddress emailAddress) { popBack(); - - final arguments = ComposerArguments( - emailActionType: EmailActionType.composeFromEmailAddress, - emailAddress: emailAddress, - mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role); - - mailboxDashBoardController.goToComposer(arguments); + mailboxDashBoardController.goToComposer(ComposerArguments.fromEmailAddress(emailAddress)); } void openMailToLink(Uri? uri) { @@ -877,12 +1015,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { log('SingleEmailController::openMailToLink(): address: $address'); if (address.isNotEmpty) { final emailAddress = EmailAddress(null, address); - final arguments = ComposerArguments( - emailActionType: EmailActionType.composeFromEmailAddress, - emailAddress: emailAddress, - mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role); - - mailboxDashBoardController.goToComposer(arguments); + mailboxDashBoardController.goToComposer(ComposerArguments.fromEmailAddress(emailAddress)); } } @@ -925,53 +1058,23 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } if (_sendReceiptToSenderInteractor == null) { - _appToast.showBottomToast( - currentOverlayContext!, - AppLocalizations.of(context).toastMessageNotSupportMdnWhenSendReceipt, - leadingIcon: SvgPicture.asset( - imagePaths.icNotConnection, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - maxWidth: responsiveUtils.getMaxWidthToast(currentContext!)); + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(context).toastMessageNotSupportMdnWhenSendReceipt); return; } if (_identitySelected == null || _identitySelected?.id == null) { - _appToast.showBottomToast( - currentOverlayContext!, - AppLocalizations.of(context).toastMessageCannotFoundIdentityWhenSendReceipt, - leadingIcon: SvgPicture.asset( - imagePaths.icNotConnection, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - maxWidth: responsiveUtils.getMaxWidthToast(currentContext!)); + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(context).toastMessageCannotFoundIdentityWhenSendReceipt); return; } if (currentEmail == null || _currentEmailId == null) { - _appToast.showBottomToast( - currentOverlayContext!, - AppLocalizations.of(context).toastMessageCannotFoundEmailIdWhenSendReceipt, - leadingIcon: SvgPicture.asset( - imagePaths.icNotConnection, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - maxWidth: responsiveUtils.getMaxWidthToast(currentContext!)); + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(context).toastMessageCannotFoundEmailIdWhenSendReceipt); return; } @@ -1021,18 +1124,10 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _sendReceiptToSenderSuccess(SendReceiptToSenderSuccess success) { log('SingleEmailController::_sendReceiptToSenderSuccess(): ${success.mdn.toString()}'); if (currentContext != null) { - _appToast.showBottomToast( - currentOverlayContext!, - AppLocalizations.of(currentContext!).toastMessageSendReceiptSuccess, - leadingIcon: SvgPicture.asset( - imagePaths.icReadReceiptMessage, - width: 24, - height: 24, - fit: BoxFit.fill), - backgroundColor: AppColor.toastSuccessBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - maxWidth: responsiveUtils.getMaxWidthToast(currentContext!)); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).toastMessageSendReceiptSuccess, + leadingSVGIcon: imagePaths.icReadReceiptMessage); } } @@ -1065,7 +1160,7 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _updateRouteOnBrowser() { log('SingleEmailController::_updateRouteOnBrowser(): isSearchEmailRunning: ${mailboxDashBoardController.searchController.isSearchEmailRunning}'); - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { final selectedMailboxId = mailboxDashBoardController.selectedMailbox.value?.id; final route = RouteUtils.generateRouteBrowser( AppRoutes.dashboard, @@ -1080,18 +1175,49 @@ class SingleEmailController extends BaseController with AppLoaderMixin { } } - void pressEmailAction(EmailActionType emailActionType) { - if (emailActionType == EmailActionType.compose) { - mailboxDashBoardController.goToComposer(ComposerArguments()); - } else { - final arguments = ComposerArguments( - emailActionType: emailActionType, - presentationEmail: mailboxDashBoardController.selectedEmail.value!, - emailContents: initialEmailContents, - attachments: emailActionType == EmailActionType.forward ? attachments : null, - mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role); - - mailboxDashBoardController.goToComposer(arguments); + void pressEmailAction( + EmailActionType emailActionType, + PresentationEmail presentationEmail + ) { + switch(emailActionType) { + case EmailActionType.compose: + mailboxDashBoardController.goToComposer(ComposerArguments()); + break; + case EmailActionType.reply: + mailboxDashBoardController.goToComposer( + ComposerArguments.replyEmail( + presentationEmail: presentationEmail, + content: _currentEmailLoaded?.htmlContent ?? '', + mailboxRole: presentationEmail.mailboxContain?.role, + messageId: _currentEmailLoaded?.emailCurrent?.messageId, + references: _currentEmailLoaded?.emailCurrent?.references, + ) + ); + break; + case EmailActionType.replyAll: + mailboxDashBoardController.goToComposer( + ComposerArguments.replyAllEmail( + presentationEmail: presentationEmail, + content: _currentEmailLoaded?.htmlContent ?? '', + mailboxRole: presentationEmail.mailboxContain?.role, + messageId: _currentEmailLoaded?.emailCurrent?.messageId, + references: _currentEmailLoaded?.emailCurrent?.references, + ) + ); + break; + case EmailActionType.forward: + mailboxDashBoardController.goToComposer( + ComposerArguments.forwardEmail( + presentationEmail: presentationEmail, + content: _currentEmailLoaded?.htmlContent ?? '', + attachments: attachments, + messageId: _currentEmailLoaded?.emailCurrent?.messageId, + references: _currentEmailLoaded?.emailCurrent?.references, + ) + ); + break; + default: + break; } } @@ -1106,25 +1232,12 @@ class SingleEmailController extends BaseController with AppLoaderMixin { session, emailAddress: emailAddress); - if (BuildUtils.isWeb) { - showDialogRuleFilterCreator( - context: context, - arguments: arguments, - onCreatedRuleFilter: (arguments) { - if (arguments is CreateNewEmailRuleFilterRequest) { - _createNewRuleFilterAction(accountId, arguments); - } - } - ); - } else { - final newRuleFilterRequest = await push( - AppRoutes.rulesFilterCreator, - arguments: arguments - ); + final newRuleFilterRequest = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.rulesFilterCreator, arguments: arguments) + : await push(AppRoutes.rulesFilterCreator, arguments: arguments); - if (newRuleFilterRequest is CreateNewEmailRuleFilterRequest) { - _createNewRuleFilterAction(accountId, newRuleFilterRequest); - } + if (newRuleFilterRequest is CreateNewEmailRuleFilterRequest) { + _createNewRuleFilterAction(accountId, newRuleFilterRequest); } } } @@ -1146,10 +1259,9 @@ class SingleEmailController extends BaseController with AppLoaderMixin { void _createNewRuleFilterSuccess(CreateNewRuleFilterSuccess success) { if (success.newListRules.isNotEmpty == true) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( + _appToast.showToastSuccessMessage( currentOverlayContext!, - message: AppLocalizations.of(currentContext!).newFilterWasCreated, - icon: imagePaths.icSelected); + AppLocalizations.of(currentContext!).newFilterWasCreated); } } } @@ -1162,10 +1274,68 @@ class SingleEmailController extends BaseController with AppLoaderMixin { emailSupervisorController.backToPreviousEmail(); } } + Future backButtonPressedCallbackAction(BuildContext context) async { - if (!BuildUtils.isWeb) { + if (PlatformInfo.isMobile) { closeEmailView(context); } return false; } + + void _storeOpenedEmailAction(Session? session, AccountId? accountId, DetailedEmail detailedEmail) async { + if (session != null && accountId != null) { + consumeState(_storeOpenedEmailInteractor.execute(session, accountId, detailedEmail)); + } + } + + bool _canParseCalendarEvent({required Set blobIds}) { + return _isCalendarEventSupported && + currentEmail?.hasCalendarEvent == true && + blobIds.isNotEmpty && + _parseCalendarEventInteractor != null; + } + + bool get _isCalendarEventSupported { + final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; + return session != null && + accountId != null && + CapabilityIdentifier.jamesCalendarEvent.isSupported(session, accountId); + } + + void _parseCalendarEventAction({ + required AccountId accountId, + required Set blobIds, + required String emailContents + }) { + log("SingleEmailController::_parseCalendarEventAction:blobIds: $blobIds"); + consumeState(_parseCalendarEventInteractor!.execute(accountId, blobIds, emailContents)); + } + + void _handleParseCalendarEventSuccess(ParseCalendarEventSuccess success) { + emailLoadedViewState.value = Right(success); + calendarEvent.value = success.calendarEventList.first; + eventActions.value = success.eventActionList; + if (PlatformInfo.isMobile) { + _enableScrollPageView(); + } + } + + void _handleParseCalendarEventFailure(ParseCalendarEventFailure failure) { + emailLoadedViewState.value = Left(failure); + emailContents.value = _currentEmailLoaded?.htmlContent; + } + + void _enableScrollPageView() { + emailSupervisorController.scrollPhysicsPageView.value = null; + } + + void openNewTabAction(String link) { + AppUtils.launchLink(link); + } + + void openNewComposerAction(String mailTo) { + final emailAddress = EmailAddress(mailTo, mailTo); + mailboxDashBoardController.goToComposer(ComposerArguments.fromEmailAddress(emailAddress)); + } } \ No newline at end of file diff --git a/lib/features/email/presentation/email_view.dart b/lib/features/email/presentation/email_view.dart index 90769534e2..ac1427b465 100644 --- a/lib/features/email/presentation/email_view.dart +++ b/lib/features/email/presentation/email_view.dart @@ -1,6 +1,5 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/icon_utils.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; @@ -8,29 +7,39 @@ import 'package:core/presentation/views/html_viewer/html_content_viewer_on_web_w import 'package:core/presentation/views/html_viewer/html_content_viewer_widget.dart'; import 'package:core/presentation/views/html_viewer/html_viewer_controller_for_web.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; -import 'package:filesize/filesize.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:model/email/attachment.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/list_attachment_extension.dart'; -import 'package:model/extensions/list_email_content_extension.dart'; +import 'package:model/extensions/list_email_address_extension.dart'; import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/widget/popup_item_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/email_view_styles.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/app_bar_mail_widget_builder.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_file_tile_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/email_attachments_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/email_subject_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_empty_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/email_view_loading_bar_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/information_sender_and_receiver_builder.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; class EmailView extends GetWidget { @@ -51,12 +60,12 @@ class EmailView extends GetWidget { if (_supportVerticalDivider(context)) const VerticalDivider( color: AppColor.lineItemListColor, - width: 1, - thickness: 0.2), + width: 12), Expanded(child: SafeArea( right: responsiveUtils.isLandscapeMobile(context), left: responsiveUtils.isLandscapeMobile(context), child: Container( + clipBehavior: Clip.antiAlias, decoration: responsiveUtils.isWebDesktop(context) ? BoxDecoration( borderRadius: BorderRadius.circular(20), @@ -66,9 +75,24 @@ class EmailView extends GetWidget { margin: _getMarginEmailView(context), child: Obx(() { if (controller.currentEmail != null) { - return _buildEmailView(context, controller.currentEmail!); + return Column(children: [ + _buildAppBar(context, controller.currentEmail!), + _buildVacationNotificationMessage(context), + const Divider(color: AppColor.colorDividerHorizontal, height: 1), + Expanded( + child: Obx(() { + return controller.emailSupervisorController.supportedPageView.isTrue + ? _buildMultipleEmailView(controller.emailSupervisorController.currentListEmail) + : _buildSingleEmailView(context, controller.currentEmail!); + }), + ), + BottomBarMailWidgetBuilder( + controller.currentEmail!, + onPressEmailActionClick: controller.pressEmailAction + ), + ]); } else { - return _buildEmailViewEmpty(context); + return const EmailViewEmptyWidget(); } }) ) @@ -79,9 +103,14 @@ class EmailView extends GetWidget { } EdgeInsets _getMarginEmailView(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (responsiveUtils.isDesktop(context)) { - return const EdgeInsets.only(right: 16, top: 16, bottom: 16); + return EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 16 : 0, + right: AppUtils.isDirectionRTL(context) ? 0 : 16, + top: 16, + bottom: 16 + ); } else { return const EdgeInsets.symmetric(vertical: 16); } @@ -90,26 +119,6 @@ class EmailView extends GetWidget { } } - Widget _buildEmailViewEmpty(BuildContext context) { - return Center(child: _buildEmailEmpty(context)); - } - - Widget _buildEmailView(BuildContext context, PresentationEmail email) { - return Column(children: [ - _buildAppBar(context, email), - _buildVacationNotificationMessage(context), - const Divider(color: AppColor.colorDividerHorizontal, height: 1), - Expanded(child: Obx(() { - return controller.emailSupervisorController.supportedPageView.isTrue - ? _buildMultipleEmailView(controller.emailSupervisorController.currentListEmail) - : _buildSingleEmailView(context, email); - }), - ), - const Divider(color: AppColor.colorDividerHorizontal, height: 1), - _buildBottomBar(context, email), - ]); - } - Widget _buildMultipleEmailView(List listEmails) { return Obx( () => PageView.builder( @@ -123,15 +132,13 @@ class EmailView extends GetWidget { ); } - Widget _buildSingleEmailView(BuildContext context, PresentationEmail email) { - return _buildEmailBody(context, email); - } - bool _supportVerticalDivider(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return responsiveUtils.isTabletLarge(context); } else { - return responsiveUtils.isLandscapeTablet(context) || responsiveUtils.isDesktop(context); + return responsiveUtils.isLandscapeTablet(context) || + responsiveUtils.isDesktop(context) || + responsiveUtils.isTabletLarge(context); } } @@ -142,15 +149,11 @@ class EmailView extends GetWidget { (responsiveUtils.isMobile(context) || responsiveUtils.isTablet(context) || responsiveUtils.isLandscapeMobile(context))) { - return Padding( - padding: const EdgeInsets.only(bottom: 5), - child: VacationNotificationMessageWidget( - radius: 0, - margin: EdgeInsets.zero, - vacationResponse: vacation!, - actionGotoVacationSetting: () => controller.mailboxDashBoardController.goToVacationSetting(), - actionEndNow: () => controller.mailboxDashBoardController.disableVacationResponder()), - ); + return VacationNotificationMessageWidget( + margin: const EdgeInsets.only(left: 12, right: 12, bottom: 5), + vacationResponse: vacation!, + actionGotoVacationSetting: controller.mailboxDashBoardController.goToVacationSetting, + actionEndNow: controller.mailboxDashBoardController.disableVacationResponder); } else { return const SizedBox.shrink(); } @@ -177,7 +180,7 @@ class EmailView extends GetWidget { _popupMenuEmailActionTile(context, email)); } }, - optionsWidget: BuildUtils.isWeb && controller.emailSupervisorController.supportedPageView.isTrue + optionsWidget: PlatformInfo.isWeb && controller.emailSupervisorController.supportedPageView.isTrue ? _buildNavigatorPageViewWidgets(context) : null, )); @@ -191,10 +194,10 @@ class EmailView extends GetWidget { return [ buildIconWeb( icon: SvgPicture.asset( - imagePaths.icNewer, - color: controller.emailSupervisorController.nextEmailActivated - ? AppColor.primaryColor - : AppColor.colorAttachmentIcon, + DirectionUtils.isDirectionRTLByLanguage(context) ? imagePaths.icOlder : imagePaths.icNewer, + colorFilter: controller.emailSupervisorController.nextEmailActivated + ? AppColor.primaryColor.asFilter() + : AppColor.colorAttachmentIcon.asFilter(), width: IconUtils.defaultIconSize, height: IconUtils.defaultIconSize, fit: BoxFit.fill), @@ -202,252 +205,190 @@ class EmailView extends GetWidget { onTap: controller.emailSupervisorController.moveToNextEmail), buildIconWeb( icon: SvgPicture.asset( - imagePaths.icOlder, + DirectionUtils.isDirectionRTLByLanguage(context) ? imagePaths.icNewer : imagePaths.icOlder, width: IconUtils.defaultIconSize, height: IconUtils.defaultIconSize, - color: controller.emailSupervisorController.previousEmailActivated - ? AppColor.primaryColor - : AppColor.colorAttachmentIcon, + colorFilter: controller.emailSupervisorController.previousEmailActivated + ? AppColor.primaryColor.asFilter() + : AppColor.colorAttachmentIcon.asFilter(), fit: BoxFit.fill), tooltip: AppLocalizations.of(context).older, onTap: controller.emailSupervisorController.backToPreviousEmail), ]; } - Widget _buildBottomBar(BuildContext context, PresentationEmail presentationEmail) { - return BottomBarMailWidgetBuilder( - presentationEmail, - onPressEmailActionClick: (emailActionType) => controller.pressEmailAction(emailActionType)); - } - - Widget _buildEmailBody(BuildContext context, PresentationEmail email) { - if (BuildUtils.isWeb) { - return _buildEmailMessage(context, email); - } else { + Widget _buildSingleEmailView(BuildContext context, PresentationEmail presentationEmail) { + if (PlatformInfo.isMobile) { return SingleChildScrollView( - primary: true, - physics : const ClampingScrollPhysics(), - child: Container( - margin: EdgeInsets.zero, + physics : const ClampingScrollPhysics(), + child: Container( + width: double.infinity, + alignment: Alignment.center, + color: Colors.white, + child: Obx(() => _buildEmailMessage( + context: context, + presentationEmail: presentationEmail, + calendarEvent: controller.calendarEvent.value + )) + ) + ); + } else { + return Obx(() { + final calendarEvent = controller.calendarEvent.value; + if (presentationEmail.hasCalendarEvent && calendarEvent != null) { + return SingleChildScrollView( + physics : const ClampingScrollPhysics(), + child: Container( width: double.infinity, alignment: Alignment.center, - padding: EdgeInsets.zero, color: Colors.white, - child: _buildEmailMessage(context, email) - ) - ); + child: _buildEmailMessage( + context: context, + presentationEmail: presentationEmail, + calendarEvent: calendarEvent, + emailAddressSender: presentationEmail.listEmailAddressSender.getListAddress(), + ) + ) + ); + } else { + return _buildEmailMessage(context: context, presentationEmail: presentationEmail); + } + }); } } - Widget _buildEmailEmpty(BuildContext context) { - return Text( - AppLocalizations.of(context).no_mail_selected, - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 25, color: AppColor.mailboxTextColor, fontWeight: FontWeight.bold)); - } - - Widget _buildEmailSubject(BuildContext context, PresentationEmail email) { - return Container( - color: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - child: SelectableText( - email.getEmailTitle(), - maxLines: BuildUtils.isWeb ? 2 : null, - minLines: BuildUtils.isWeb ? 1 : null, - cursorColor: AppColor.colorTextButton, - style: const TextStyle( - fontSize: 20, - color: AppColor.colorNameEmail, - fontWeight: FontWeight.w500) - )); - } - - Widget _buildEmailMessage(BuildContext context, PresentationEmail email) { + Widget _buildEmailMessage({ + required BuildContext context, + required PresentationEmail presentationEmail, + CalendarEvent? calendarEvent, + List? emailAddressSender, + }) { return LayoutBuilder( builder: (context, constraints) { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildEmailSubject(context, email), + EmailSubjectWidget(presentationEmail: presentationEmail), InformationSenderAndReceiverBuilder( controller: controller, - emailSelected: email, + emailSelected: presentationEmail, imagePaths: imagePaths, responsiveUtils: responsiveUtils, ), - _buildLoadingView(), - _buildAttachments(context), - if (BuildUtils.isWeb) - Expanded(child: Padding( - padding: const EdgeInsets.only(left: 16, bottom: 16), - child: _buildEmailContent(context, constraints, email))) + Obx(() { + final attachments = controller.attachments.listAttachmentsDisplayedOutSide; + if (attachments.isNotEmpty) { + return EmailAttachmentsWidget( + attachments: attachments, + onDragStarted: () { + log('EmailView::_buildEmailMessage:onDragStarted:'); + controller.mailboxDashBoardController.enableDraggableApp(); + }, + onDragEnd: (details) { + log('EmailView::_buildEmailMessage:onDragEnd:'); + controller.mailboxDashBoardController.disableDraggableApp(); + }, + downloadAttachmentAction: (attachment) { + if (PlatformInfo.isWeb) { + controller.downloadAttachmentForWeb(context, attachment); + } else { + controller.exportAttachment(context, attachment); + } + }, + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() => EmailViewLoadingBarWidget( + viewState: controller.emailLoadedViewState.value + )), + if (calendarEvent != null) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + CalendarEventInformationWidget( + calendarEvent: calendarEvent, + eventActions: controller.eventActions, + onOpenComposerAction: controller.openNewComposerAction, + onOpenNewTabAction: controller.openNewTabAction, + ), + if (calendarEvent.getTitleEventAction(context, emailAddressSender ?? []).isNotEmpty) + CalendarEventActionBannerWidget( + calendarEvent: calendarEvent, + listEmailAddressSender: emailAddressSender ?? [] + ), + CalendarEventDetailWidget( + calendarEvent: calendarEvent, + eventActions: controller.eventActions, + onOpenComposerAction: controller.openNewComposerAction, + onOpenNewTabAction: controller.openNewTabAction, + ), + ], + ) else - Padding( - padding: const EdgeInsets.all(16), - child: _buildEmailContent(context, constraints, email)) + _buildEmailContentOnHtmlViewer(context, constraints, presentationEmail) ], ); }); } - Widget _buildLoadingView() { - return Obx(() { - return controller.viewState.value.fold( - (failure) => const SizedBox.shrink(), - (success) { - if (success is LoadingState) { - return const Align(alignment: Alignment.topCenter, child: Padding( - padding: EdgeInsets.all(16), - child: SizedBox( - width: 30, - height: 30, - child: CupertinoActivityIndicator(color: AppColor.colorLoading)))); - } else { - return const SizedBox.shrink(); - } - }); - }); - } - - Widget _buildAttachments(BuildContext context) { - return Obx(() { - final attachments = controller.attachments.listAttachmentsDisplayedOutSide; - return attachments.isNotEmpty - ? _buildAttachmentsBody(context, attachments) - : const SizedBox.shrink(); - }); - } - - Widget _buildAttachmentsBody(BuildContext context, List attachments) { - return Container( - color: Colors.white, - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 12, top: 10), - child: Column(crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildAttachmentsHeader(context, attachments), - _buildAttachmentsList(context, attachments, controller.isDisplayFullAttachments) - ], - ), - ); - } - - Widget _buildAttachmentsHeader(BuildContext context, List attachments) { - return Container( - color: Colors.white, - child: Row( - children: [ - Expanded(child: Container( - margin: const EdgeInsets.symmetric(vertical: 4), - child: Row(children: [ - SvgPicture.asset(imagePaths.icAttachment, - width: 20, - height: 20, - color: AppColor.colorAttachmentIcon, - fit: BoxFit.fill), - const SizedBox(width: 5), - Expanded(child: Text( - AppLocalizations.of(context).titleHeaderAttachment( - attachments.length, - filesize(attachments.totalSize(), 1)), - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.normal, - color: AppColor.colorTitleHeaderAttachment))) - ]) - )), - if (attachments.length > 2) - Obx(() => Material( - color: Colors.transparent, - child: InkWell( - onTap: controller.toggleDisplayAttachmentsAction, - customBorder: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), - child: Text( - controller.isDisplayFullAttachments - ? AppLocalizations.of(context).hide - : AppLocalizations.of(context).showAll, - style: const TextStyle( - fontSize: 13, - color: AppColor.colorTextButton, - fontWeight: FontWeight.normal)), - ), - ), - )) - ], - ), - ); - } - - Widget _buildAttachmentsList( - BuildContext context, - List attachments, - bool isDisplayAll + Widget _buildEmailContentOnHtmlViewer( + BuildContext context, + BoxConstraints constraints, + PresentationEmail email ) { - return LayoutBuilder(builder: (context, constraints) { - if (isDisplayAll) { - return Wrap( - runSpacing: 12, - children: attachments - .map((attachment) => AttachmentFileTileBuilder( - attachment, - onDownloadAttachmentFileActionClick: (attachment) { - if (BuildUtils.isWeb) { - controller.downloadAttachmentForWeb(context, attachment); - } else { - controller.exportAttachment(context, attachment); - } - })) - .toList()); - } else { - return Container( - height: 60, - color: Colors.transparent, - child: ListView.builder( - key: const Key('list_attachment_minimize_in_email'), - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - scrollDirection: Axis.horizontal, - itemCount: attachments.length, - itemBuilder: (context, index) => AttachmentFileTileBuilder( - attachments[index], - onDownloadAttachmentFileActionClick: (attachment) { - if (BuildUtils.isWeb) { - controller.downloadAttachmentForWeb(context, attachment); - } else { - controller.exportAttachment(context, attachment); - } - }) - ) - ); - } - }); - } - - Widget _buildEmailContent(BuildContext context, BoxConstraints constraints, PresentationEmail email) { - if(email.id != controller.currentEmail?.id) { + if (email.id != controller.currentEmail?.id) { return const SizedBox.shrink(); } return Obx(() { - if (controller.emailContents.isNotEmpty) { - final allEmailContents = controller.emailContents.asHtmlString; - - if (BuildUtils.isWeb) { - return HtmlContentViewerOnWeb( - widthContent: constraints.maxWidth, - heightContent: responsiveUtils.getSizeScreenHeight(context), - contentHtml: allEmailContents, - controller: HtmlViewerControllerForWeb(), - mailtoDelegate: (uri) => controller.openMailToLink(uri)); + if (controller.emailContents.value != null) { + final allEmailContents = controller.emailContents.value; + + if (PlatformInfo.isWeb) { + return Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 16, bottom: 16), + child: Obx(() { + return Stack( + children: [ + HtmlContentViewerOnWeb( + widthContent: constraints.maxWidth, + heightContent: responsiveUtils.getSizeScreenHeight(context), + contentHtml: allEmailContents ?? "", + controller: HtmlViewerControllerForWeb(), + mailtoDelegate: (uri) => controller.openMailToLink(uri), + direction: AppUtils.getCurrentDirection(context), + ), + if (controller.mailboxDashBoardController.isDraggableAppActive) + PointerInterceptor( + child: SizedBox( + width: constraints.maxWidth, + height: constraints.maxHeight, + ) + ) + ], + ); + }), + ), + ); } else { - return HtmlContentViewer( - heightContent: responsiveUtils.getSizeScreenHeight(context), - contentHtml: allEmailContents, - mailtoDelegate: (uri) async => controller.openMailToLink(uri), - onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView, - onWebViewLoaded: (isScrollPageViewActivated) { - log('EmailView::_buildEmailContent(): isScrollPageViewActivated: $isScrollPageViewActivated'); - controller.emailSupervisorController.updateScrollPhysicPageView(isScrollPageViewActivated: isScrollPageViewActivated); - }, + return Padding( + padding: const EdgeInsetsDirectional.symmetric( + vertical: EmailViewStyles.mobileContentVerticalMargin, + horizontal: EmailViewStyles.mobileContentHorizontalMargin + ), + child: HtmlContentViewer( + heightContent: responsiveUtils.getSizeScreenHeight(context), + contentHtml: allEmailContents ?? "", + mailtoDelegate: (uri) async => controller.openMailToLink(uri), + onScrollHorizontalEnd: controller.toggleScrollPhysicsPagerView, + onWebViewLoaded: (isScrollPageViewActivated) { + log('EmailView::_buildEmailContent(): isScrollPageViewActivated: $isScrollPageViewActivated'); + controller.emailSupervisorController.updateScrollPhysicPageView(isScrollPageViewActivated: isScrollPageViewActivated); + }, + direction: AppUtils.getCurrentDirection(context), + ), ); } } else { @@ -472,15 +413,24 @@ class EmailView extends GetWidget { width: 24, height: 24, fit: BoxFit.fill, - color: AppColor.colorTextButton + colorFilter: AppColor.colorTextButton.asFilter() ), AppLocalizations.of(context).mark_as_unread, email, iconLeftPadding: responsiveUtils.isMobile(context) - ? const EdgeInsets.only(left: 12, right: 16) - : const EdgeInsets.only(right: 12), + ? EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 16 : 12, + right: AppUtils.isDirectionRTL(context) ? 12 : 16, + ) + : EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 12 : 0, + right: AppUtils.isDirectionRTL(context) ? 0 : 12, + ), iconRightPadding: responsiveUtils.isMobile(context) - ? const EdgeInsets.only(right: 12) + ? EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 12 : 0, + right: AppUtils.isDirectionRTL(context) ? 0 : 12, + ) : EdgeInsets.zero) ..onActionClick((email) => controller.handleEmailAction( context, @@ -500,17 +450,26 @@ class EmailView extends GetWidget { width: 24, height: 24, fit: BoxFit.fill, - color: AppColor.colorTextButton + colorFilter: AppColor.colorTextButton.asFilter() ), currentMailbox?.isSpam == true ? AppLocalizations.of(context).remove_from_spam : AppLocalizations.of(context).mark_as_spam, email, iconLeftPadding: responsiveUtils.isMobile(context) - ? const EdgeInsets.only(left: 12, right: 16) - : const EdgeInsets.only(right: 12), + ? EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 16 : 12, + right: AppUtils.isDirectionRTL(context) ? 12 : 16, + ) + : EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 12 : 0, + right: AppUtils.isDirectionRTL(context) ? 0 : 12, + ), iconRightPadding: responsiveUtils.isMobile(context) - ? const EdgeInsets.only(right: 12) + ? EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 12 : 0, + right: AppUtils.isDirectionRTL(context) ? 0 : 12, + ) : EdgeInsets.zero) ..onActionClick((email) => controller.handleEmailAction( context, @@ -528,14 +487,23 @@ class EmailView extends GetWidget { width: 24, height: 24, fit: BoxFit.fill, - color: AppColor.colorTextButton), + colorFilter: AppColor.colorTextButton.asFilter()), AppLocalizations.of(context).quickCreatingRule, email, iconLeftPadding: responsiveUtils.isMobile(context) - ? const EdgeInsets.only(left: 12, right: 16) - : const EdgeInsets.only(right: 12), + ? EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 16 : 12, + right: AppUtils.isDirectionRTL(context) ? 12 : 16, + ) + : EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 12 : 0, + right: AppUtils.isDirectionRTL(context) ? 0 : 12, + ), iconRightPadding: responsiveUtils.isMobile(context) - ? const EdgeInsets.only(right: 12) + ? EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 12 : 0, + right: AppUtils.isDirectionRTL(context) ? 0 : 12, + ) : EdgeInsets.zero) ..onActionClick((email) => controller.quickCreatingRule(context, email.from!.first))) .build(); @@ -557,6 +525,7 @@ class EmailView extends GetWidget { imagePaths.icUnreadEmail, AppLocalizations.of(context).mark_as_unread, colorIcon: AppColor.colorTextButton, + padding: const EdgeInsetsDirectional.only(start: 12), styleName: const TextStyle( fontWeight: FontWeight.w500, fontSize: 16, @@ -584,6 +553,7 @@ class EmailView extends GetWidget { ? AppLocalizations.of(context).remove_from_spam : AppLocalizations.of(context).mark_as_spam, colorIcon: AppColor.colorTextButton, + padding: const EdgeInsetsDirectional.only(start: 12), styleName: const TextStyle( fontWeight: FontWeight.w500, fontSize: 16, @@ -605,6 +575,7 @@ class EmailView extends GetWidget { imagePaths.icQuickCreatingRule, AppLocalizations.of(context).quickCreatingRule, colorIcon: AppColor.colorTextButton, + padding: const EdgeInsetsDirectional.only(start: 12), styleName: const TextStyle( fontWeight: FontWeight.w500, fontSize: 16, diff --git a/lib/features/email/presentation/extensions/calendar_event_extension.dart b/lib/features/email/presentation/extensions/calendar_event_extension.dart new file mode 100644 index 0000000000..ae4d8fad44 --- /dev/null +++ b/lib/features/email/presentation/extensions/calendar_event_extension.dart @@ -0,0 +1,299 @@ + +import 'package:collection/collection.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:date_format/date_format.dart' as date_format; +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee_participation_status.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/event_method.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; + +extension CalendarEventExtension on CalendarEvent { + + Color getColorEventActionBanner(List listEmailAddressSender) { + switch(method) { + case EventMethod.request: + case EventMethod.add: + return AppColor.colorInvitedEventActionText; + case EventMethod.refresh: + case EventMethod.counter: + return AppColor.colorUpdatedEventActionText; + case EventMethod.cancel: + case EventMethod.declineCounter: + return AppColor.colorCanceledEventActionText; + case EventMethod.reply: + final matchedAttendee = findAttendeeHasUpdatedStatus(listEmailAddressSender); + if (matchedAttendee != null) { + return getAttendeeMessageTextColor(matchedAttendee.participationStatus); + } else { + return Colors.transparent; + } + default: + return Colors.transparent; + } + } + + Color getColorEventActionText(List listEmailAddressSender) { + switch(method) { + case EventMethod.request: + case EventMethod.add: + return AppColor.colorInvitedEventActionText; + case EventMethod.refresh: + case EventMethod.counter: + return AppColor.colorUpdatedEventActionText; + case EventMethod.cancel: + case EventMethod.declineCounter: + return AppColor.colorCanceledEventActionText; + case EventMethod.reply: + final matchedAttendee = findAttendeeHasUpdatedStatus(listEmailAddressSender); + if (matchedAttendee != null) { + return getAttendeeMessageTextColor(matchedAttendee.participationStatus); + } else { + return Colors.transparent; + } + default: + return Colors.transparent; + } + } + + String getIconEventAction(ImagePaths imagePaths) { + switch(method) { + case EventMethod.request: + case EventMethod.add: + return imagePaths.icEventInvited; + case EventMethod.refresh: + return imagePaths.icEventUpdated; + case EventMethod.cancel: + return imagePaths.icEventCanceled; + default: + return ''; + } + } + + String getTitleEventAction(BuildContext context, List listEmailAddressSender) { + switch(method) { + case EventMethod.request: + case EventMethod.add: + return AppLocalizations.of(context).messageEventActionBannerOrganizerInvited; + case EventMethod.refresh: + return AppLocalizations.of(context).messageEventActionBannerOrganizerUpdated; + case EventMethod.cancel: + return AppLocalizations.of(context).messageEventActionBannerOrganizerCanceled; + case EventMethod.reply: + final matchedAttendee = findAttendeeHasUpdatedStatus(listEmailAddressSender); + if (matchedAttendee != null) { + return getAttendeeMessageStatus(context, matchedAttendee.participationStatus); + } else { + return ''; + } + case EventMethod.counter: + return AppLocalizations.of(context).messageEventActionBannerAttendeeCounter; + case EventMethod.declineCounter: + return AppLocalizations.of(context).messageEventActionBannerAttendeeCounterDeclined; + default: + return ''; + } + } + + String getSubTitleEventAction(BuildContext context) { + switch(method) { + case EventMethod.refresh: + return AppLocalizations.of(context).subMessageEventActionBannerUpdated; + case EventMethod.cancel: + return AppLocalizations.of(context).subMessageEventActionBannerCanceled; + default: + return ''; + } + } + + String getUserNameEventAction({ + required BuildContext context, + required ImagePaths imagePaths, + required List listEmailAddressSender + }) { + switch(method) { + case EventMethod.request: + case EventMethod.add: + case EventMethod.refresh: + case EventMethod.cancel: + case EventMethod.declineCounter: + return getOrganizerNameEvent(context); + case EventMethod.reply: + case EventMethod.counter: + return getAttendeeNameEvent(context, listEmailAddressSender); + default: + return ''; + } + } + + String getOrganizerNameEvent(BuildContext context) => organizer?.name ?? AppLocalizations.of(context).you; + + String get organizerName => organizer?.name ?? organizer?.mailto?.value ?? ''; + + String getAttendeeNameEvent(BuildContext context, List listEmailAddressSender) { + final matchedAttendee = findAttendeeHasUpdatedStatus(listEmailAddressSender); + if (matchedAttendee != null) { + return matchedAttendee.name?.name ?? AppLocalizations.of(context).anAttendee; + } else { + return AppLocalizations.of(context).anAttendee; + } + } + + CalendarAttendee? findAttendeeHasUpdatedStatus(List listEmailAddressSender) { + if (participants?.isNotEmpty == true) { + final listMatchedAttendee = participants + !.where((attendee) => attendee.mailto != null && listEmailAddressSender.contains(attendee.mailto!.mailAddress.value)) + .whereNotNull(); + log('CalendarEventExtension::findAttendeeHasUpdatedStatus:listMatchedAttendee: $listMatchedAttendee'); + if (listMatchedAttendee.isNotEmpty) { + return listMatchedAttendee.first; + } + } + return null; + } + + String getAttendeeMessageStatus(BuildContext context, CalendarAttendeeParticipationStatus? status) { + if (status == CalendarAttendeeParticipationStatus('ACCEPTED')) { + return AppLocalizations.of(context).messageEventActionBannerAttendeeAccepted; + } else if (status == CalendarAttendeeParticipationStatus('TENTATIVE')) { + return AppLocalizations.of(context).messageEventActionBannerAttendeeTentative; + } else if (status == CalendarAttendeeParticipationStatus('DECLINED')) { + return AppLocalizations.of(context).messageEventActionBannerAttendeeDeclined; + } else { + return ''; + } + } + + Color getAttendeeMessageTextColor(CalendarAttendeeParticipationStatus? status) { + if (status == CalendarAttendeeParticipationStatus('ACCEPTED')) { + return AppColor.colorUpdatedEventActionText; + } else if (status == CalendarAttendeeParticipationStatus('TENTATIVE')) { + return AppColor.colorMaybeEventActionText; + } else if (status == CalendarAttendeeParticipationStatus('DECLINED')) { + return AppColor.colorCanceledEventActionText; + } else { + return Colors.transparent; + } + } + + DateTime? get localStartDate => startUtcDate?.value.toLocal(); + + DateTime? get localEndDate => endUtcDate?.value.toLocal(); + + String get monthStartDateAsString { + if (localStartDate != null) { + return date_format.formatDate( + localStartDate!, + [date_format.M], + locale: AppUtils.getCurrentDateLocale() + ); + } else { + return ''; + } + } + + String get dayStartDateAsString { + if (localStartDate != null) { + return date_format.formatDate( + localStartDate!, + [date_format.d], + locale: AppUtils.getCurrentDateLocale() + ); + } else { + return ''; + } + } + + String get weekDayStartDateAsString { + if (localStartDate != null) { + return date_format.formatDate( + localStartDate!, + [date_format.D], + locale: AppUtils.getCurrentDateLocale() + ); + } else { + return ''; + } + } + + String formatDateTime(DateTime dateTime) { + return date_format.formatDate( + dateTime, + [ + date_format.DD, + ', ', + date_format.MM, + ' ', + date_format.dd, + ', ', + date_format.yyyy, + ' ', + date_format.hh, + ':', + date_format.ss, + ' ', + date_format.am + ], + locale: AppUtils.getCurrentDateLocale() + ); + } + + String formatTime(DateTime dateTime) { + return date_format.formatDate( + dateTime, + [ + date_format.hh, + ':', + date_format.ss, + ' ', + date_format.am + ], + locale: AppUtils.getCurrentDateLocale() + ); + } + + String get dateTimeEventAsString { + if (localStartDate != null && localEndDate != null) { + final timeStart = formatDateTime(localStartDate!); + final timeEnd = DateUtils.isSameDay(localStartDate, localEndDate) + ? formatTime(localEndDate!) + : formatDateTime(localEndDate!); + return '$timeStart - $timeEnd'; + } else if (localStartDate != null) { + return formatDateTime(localStartDate!); + } else if (localEndDate != null) { + return formatDateTime(localEndDate!); + } else { + return ''; + } + } + + List get videoConferences { + if (extensionFields != null && extensionFields!.mapFields.isNotEmpty) { + final videoConferences = List.empty(growable: true); + + final openPaasVideoConferences = extensionFields?.mapFields['X-OPENPAAS-VIDEOCONFERENCE'] + ?.whereNotNull() + .where((link) => link.isNotEmpty) + .toList() ?? []; + log('CalendarEventExtension::openPaasVideoConferences: $openPaasVideoConferences'); + final googleVideoConferences = extensionFields!.mapFields['X-GOOGLE-CONFERENCE'] + ?.whereNotNull() + .where((link) => link.isNotEmpty) + .toList() ?? []; + log('CalendarEventExtension::googleVideoConferences: $googleVideoConferences'); + if (openPaasVideoConferences.isNotEmpty) { + videoConferences.addAll(openPaasVideoConferences); + } + if (googleVideoConferences.isNotEmpty) { + videoConferences.addAll(googleVideoConferences); + } + return videoConferences; + } + return []; + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/extensions/list_attendee_extension.dart b/lib/features/email/presentation/extensions/list_attendee_extension.dart new file mode 100644 index 0000000000..248fd32b48 --- /dev/null +++ b/lib/features/email/presentation/extensions/list_attendee_extension.dart @@ -0,0 +1,19 @@ + +import 'package:collection/collection.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; + +extension ListAttendeeExtension on List { + + String get mailtoAsString { + return map((attendee) => attendee.mailto?.mailAddress.value) + .whereNotNull() + .join(', '); + } + + List withoutOrganizer(CalendarOrganizer organizer) { + return where((attendee) => attendee.mailto?.mailAddress != organizer.mailto) + .whereNotNull() + .toList(); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/model/composer_arguments.dart b/lib/features/email/presentation/model/composer_arguments.dart index 4a0abfd992..4f6b969e9e 100644 --- a/lib/features/email/presentation/model/composer_arguments.dart +++ b/lib/features/email/presentation/model/composer_arguments.dart @@ -1,17 +1,25 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; import 'package:tmail_ui_user/main/routes/router_arguments.dart'; class ComposerArguments extends RouterArguments { final EmailActionType emailActionType; final PresentationEmail? presentationEmail; - final List? emailContents; + final String? emailContents; final List? listSharedMediaFile; final EmailAddress? emailAddress; final List? attachments; final Role? mailboxRole; + final SendingEmail? sendingEmail; + final String? subject; + final MessageIdsHeaderValue? messageId; + final MessageIdsHeaderValue? references; ComposerArguments({ this.emailActionType = EmailActionType.compose, @@ -21,8 +29,113 @@ class ComposerArguments extends RouterArguments { this.mailboxRole, this.emailAddress, this.listSharedMediaFile, + this.sendingEmail, + this.subject, + this.messageId, + this.references, }); + factory ComposerArguments.fromSendingEmail(SendingEmail sendingEmail) => + ComposerArguments( + emailActionType: EmailActionType.editSendingEmail, + sendingEmail: sendingEmail + ); + + factory ComposerArguments.fromContentShared(String content) => + ComposerArguments( + emailActionType: EmailActionType.composeFromContentShared, + emailContents: content + ); + + factory ComposerArguments.fromFileShared(List filesShared) => + ComposerArguments( + emailActionType: EmailActionType.composeFromFileShared, + listSharedMediaFile: filesShared + ); + + factory ComposerArguments.fromEmailAddress(EmailAddress emailAddress) => + ComposerArguments( + emailActionType: EmailActionType.composeFromEmailAddress, + emailAddress: emailAddress + ); + + factory ComposerArguments.fromMailtoUri({EmailAddress? emailAddress, String? subject}) => + ComposerArguments( + emailActionType: EmailActionType.composeFromMailtoUri, + emailAddress: emailAddress, + subject: subject, + ); + + factory ComposerArguments.editDraftEmail(PresentationEmail presentationEmail) => + ComposerArguments( + emailActionType: EmailActionType.editDraft, + presentationEmail: presentationEmail + ); + + factory ComposerArguments.fromSessionStorageBrowser(ComposerCache composerCache) => + ComposerArguments( + emailActionType: EmailActionType.reopenComposerBrowser, + presentationEmail: PresentationEmail( + id: composerCache.id, + subject: composerCache.subject, + from: composerCache.from, + to: composerCache.to, + cc: composerCache.cc, + bcc: composerCache.bcc, + ), + emailContents: composerCache.emailContentList.asHtmlString, + ); + + factory ComposerArguments.replyEmail({ + required PresentationEmail presentationEmail, + required String content, + Role? mailboxRole, + MessageIdsHeaderValue? messageId, + MessageIdsHeaderValue? references, + }) => ComposerArguments( + emailActionType: EmailActionType.reply, + presentationEmail: presentationEmail, + emailContents: content, + mailboxRole: mailboxRole, + messageId: messageId, + references: references, + ); + + factory ComposerArguments.replyAllEmail({ + required PresentationEmail presentationEmail, + required String content, + Role? mailboxRole, + MessageIdsHeaderValue? messageId, + MessageIdsHeaderValue? references, + }) => ComposerArguments( + emailActionType: EmailActionType.replyAll, + presentationEmail: presentationEmail, + emailContents: content, + mailboxRole: mailboxRole, + messageId: messageId, + references: references, + ); + + factory ComposerArguments.forwardEmail({ + required PresentationEmail presentationEmail, + required String content, + required List attachments, + MessageIdsHeaderValue? messageId, + MessageIdsHeaderValue? references, + }) => ComposerArguments( + emailActionType: EmailActionType.forward, + presentationEmail: presentationEmail, + emailContents: content, + attachments: attachments, + mailboxRole: presentationEmail.mailboxContain?.role, + messageId: messageId, + references: references, + ); + + SendingEmailActionType get sendingEmailActionType => sendingEmail != null + ? SendingEmailActionType.edit + : SendingEmailActionType.create; + @override List get props => [ emailActionType, @@ -32,5 +145,9 @@ class ComposerArguments extends RouterArguments { mailboxRole, emailAddress, listSharedMediaFile, + sendingEmail, + subject, + messageId, + references, ]; } \ No newline at end of file diff --git a/lib/features/email/presentation/model/email_loaded.dart b/lib/features/email/presentation/model/email_loaded.dart index a35f15b1fc..948a684216 100644 --- a/lib/features/email/presentation/model/email_loaded.dart +++ b/lib/features/email/presentation/model/email_loaded.dart @@ -1,24 +1,21 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; -import 'package:model/model.dart'; +import 'package:model/email/attachment.dart'; class EmailLoaded with EquatableMixin { - final List emailContents; - final List emailContentsDisplayed; + final String htmlContent; final List attachments; final Email? emailCurrent; - EmailLoaded( - this.emailContents, - this.emailContentsDisplayed, - this.attachments, + EmailLoaded({ + required this.htmlContent, + required this.attachments, this.emailCurrent, - ); + }); @override List get props => [ - emailContents, - emailContentsDisplayed, + htmlContent, attachments, emailCurrent ]; diff --git a/lib/features/email/presentation/styles/attachment/attachment_item_widget_style.dart b/lib/features/email/presentation/styles/attachment/attachment_item_widget_style.dart new file mode 100644 index 0000000000..9410d7d4f9 --- /dev/null +++ b/lib/features/email/presentation/styles/attachment/attachment_item_widget_style.dart @@ -0,0 +1,35 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AttachmentItemWidgetStyle { + static const double radius = 10; + static const double mobileWidth = 224; + static const double width = 250; + static const double height = 60; + static const double iconSize = 44; + static const double space = 8; + static const double downloadIconSize = 24; + + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.only(end: 12); + static const EdgeInsetsGeometry contentPadding = EdgeInsetsDirectional.all(8); + + static const Color borderColor = AppColor.attachmentFileBorderColor; + static const Color downloadIconColor = AppColor.primaryColor; + + static const TextStyle labelTextStyle = TextStyle( + fontSize: 14, + color: AppColor.attachmentFileNameColor, + fontWeight: FontWeight.normal + ); + static const TextStyle dotsLabelTextStyle = TextStyle( + fontSize: 12, + color: AppColor.attachmentFileNameColor, + fontWeight: FontWeight.normal + ); + static const TextStyle sizeLabelTextStyle = TextStyle( + fontSize: 12, + color: AppColor.attachmentFileSizeColor, + fontWeight: FontWeight.normal + ); +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/attachment/feedback_draggable_attachment_item_widget_style.dart b/lib/features/email/presentation/styles/attachment/feedback_draggable_attachment_item_widget_style.dart new file mode 100644 index 0000000000..0bc6944ed7 --- /dev/null +++ b/lib/features/email/presentation/styles/attachment/feedback_draggable_attachment_item_widget_style.dart @@ -0,0 +1,38 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class FeedbackDraggableAttachmentItemWidgetStyle { + static const double radius = 8; + static const double iconSize = 24; + static const double space = 12; + static const double maxWidth = 300; + + static const Color backgroundColor = Colors.white; + + static const EdgeInsetsGeometry padding = EdgeInsets.symmetric(horizontal: 16, vertical: 12); + + static const TextStyle labelTextStyle = TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w500 + ); + static const TextStyle dotsLabelTextStyle = TextStyle( + fontSize: 12, + color: Colors.black, + fontWeight: FontWeight.w500 + ); + + static const List shadows = [ + BoxShadow( + color: AppColor.colorShadowLayerBottom, + blurRadius: 96, + offset: Offset.zero + ), + BoxShadow( + color: AppColor.colorShadowLayerTop, + blurRadius: 2, + offset: Offset.zero + ), + ]; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/attendee_widget_styles.dart b/lib/features/email/presentation/styles/attendee_widget_styles.dart new file mode 100644 index 0000000000..0a66107c97 --- /dev/null +++ b/lib/features/email/presentation/styles/attendee_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AttendeeWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color textColor = Colors.black; + static const Color mailtoColor = AppColor.colorMailto; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/calendar_date_icon_widget_styles.dart b/lib/features/email/presentation/styles/calendar_date_icon_widget_styles.dart new file mode 100644 index 0000000000..a2e1854960 --- /dev/null +++ b/lib/features/email/presentation/styles/calendar_date_icon_widget_styles.dart @@ -0,0 +1,11 @@ + +class CalendarIconWidgetStyles { + static const double borderRadius = 8; + static const double headerVerticalContentPadding = 4; + static const double headerHorizontalContentPadding = 8; + static const double headerTextSize = 14; + static const double bodyDayTextSize = 40; + static const double bodyWeekDayTextSize = 14; + static const double bodyContentPadding = 8; + static const double margin = 16; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/calendar_event_action_banner_styles.dart b/lib/features/email/presentation/styles/calendar_event_action_banner_styles.dart new file mode 100644 index 0000000000..fc4203ca57 --- /dev/null +++ b/lib/features/email/presentation/styles/calendar_event_action_banner_styles.dart @@ -0,0 +1,10 @@ + +class CalendarEventActionBannerStyles { + static const double borderRadius = 12; + static const double contentPadding = 12; + static const double viewHorizontalMargin = 16; + static const double viewVerticalMargin = 12; + static const double titleTextSize = 16; + static const double subTileTextSize = 13; + static const double iconSize = 20; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/calendar_event_action_button_widget_styles.dart b/lib/features/email/presentation/styles/calendar_event_action_button_widget_styles.dart new file mode 100644 index 0000000000..9a88292bac --- /dev/null +++ b/lib/features/email/presentation/styles/calendar_event_action_button_widget_styles.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class CalendarEventActionButtonWidgetStyles { + static const double borderRadius = 10; + static const double textSize = 16; + static const double space = 16; + static const double borderWidth = 1; + static const double minWidth = 80; + + static const Color backgroundColor = Colors.transparent; + static const Color textColor = AppColor.primaryColor; + + static const FontWeight fontWeight = FontWeight.w500; + + static const EdgeInsetsGeometry buttonPadding = EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 12); + static const EdgeInsetsGeometry paddingMobile = EdgeInsetsDirectional.only(top: 16); + static const EdgeInsetsGeometry paddingWeb = EdgeInsetsDirectional.only(start: 100, end: 16, top: 16); + static const EdgeInsetsGeometry margin = EdgeInsetsDirectional.only(top: 16); +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/calendar_event_detail_widget_styles.dart b/lib/features/email/presentation/styles/calendar_event_detail_widget_styles.dart new file mode 100644 index 0000000000..80d262e2d9 --- /dev/null +++ b/lib/features/email/presentation/styles/calendar_event_detail_widget_styles.dart @@ -0,0 +1,14 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class CalendarEventDetailWidgetStyles { + static const double textSize = 24; + static const double borderRadius = 16; + static const double verticalMargin = 12; + static const double horizontalMargin = 16; + static const double contentPadding = 16; + static const double fieldTopPadding = 16; + static const Color borderStrokeColor = AppColor.colorCalendarEventInformationStroke; + static const double borderStrokeWidth = 0.5; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/calendar_event_information_widget_styles.dart b/lib/features/email/presentation/styles/calendar_event_information_widget_styles.dart new file mode 100644 index 0000000000..507a7b643c --- /dev/null +++ b/lib/features/email/presentation/styles/calendar_event_information_widget_styles.dart @@ -0,0 +1,18 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class CalendarEventInformationWidgetStyles { + static const double borderRadius = 16; + static const double contentPadding = 16; + static const double verticalMargin = 12; + static const double horizontalMargin = 16; + static const double calendarDateIconMargin = 16; + static const Color calendarDateIconBackgroundColor = AppColor.colorCalendarEventInformationBackground; + static const double calendarInformationMargin = 16; + static const double invitationMessageTextSize = 16; + static const double fieldTopPadding = 16; + static const double space = 8; + static const Color titleColor = Colors.black; + static const Color invitationMessageColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/email_attachments_styles.dart b/lib/features/email/presentation/styles/email_attachments_styles.dart new file mode 100644 index 0000000000..51a17130a7 --- /dev/null +++ b/lib/features/email/presentation/styles/email_attachments_styles.dart @@ -0,0 +1,24 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EmailAttachmentsStyles { + static const double headerTextSize = 15; + static const double headerIconSize = 20; + static const double headerSpace = 4; + static const double marginHeader = 6; + static const double buttonTextSize = 13; + static const double buttonBorderRadius = 8; + static const double listSpace = 12; + static const double listHeight = 60; + + static const Color headerTextColor = AppColor.colorTitleHeaderAttachment; + static const Color headerIconColor = AppColor.colorAttachmentIcon; + static const Color buttonTextColor = AppColor.primaryColor; + + static const FontWeight headerFontWeight = FontWeight.w400; + static const FontWeight buttonFontWeight = FontWeight.w400; + + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 16); + static const EdgeInsetsGeometry buttonPadding = EdgeInsets.symmetric(vertical: 8, horizontal: 12); +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/email_subject_styles.dart b/lib/features/email/presentation/styles/email_subject_styles.dart new file mode 100644 index 0000000000..177944a342 --- /dev/null +++ b/lib/features/email/presentation/styles/email_subject_styles.dart @@ -0,0 +1,17 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; + +class EmailSubjectStyles { + static const double textSize = 20; + static const int? maxLines = PlatformInfo.isWeb ? 2 : null; + static const int? minLines = PlatformInfo.isWeb ? 1 : null; + + static const Color textColor = AppColor.colorNameEmail; + static const Color cursorColor = AppColor.colorTextButton; + + static const FontWeight fontWeight = FontWeight.w500; + + static const EdgeInsetsGeometry padding = EdgeInsets.symmetric(vertical: 12, horizontal: 16); +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/email_view_empty_styles.dart b/lib/features/email/presentation/styles/email_view_empty_styles.dart new file mode 100644 index 0000000000..de4fb35eb1 --- /dev/null +++ b/lib/features/email/presentation/styles/email_view_empty_styles.dart @@ -0,0 +1,11 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EmailViewEmptyStyles { + static const double textSize = 25; + + static const Color textColor = AppColor.mailboxTextColor; + + static const FontWeight fontWeight = FontWeight.bold; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/email_view_styles.dart b/lib/features/email/presentation/styles/email_view_styles.dart new file mode 100644 index 0000000000..1f331a7560 --- /dev/null +++ b/lib/features/email/presentation/styles/email_view_styles.dart @@ -0,0 +1,5 @@ + +class EmailViewStyles { + static const double mobileContentHorizontalMargin = 16; + static const double mobileContentVerticalMargin = 12; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_attendee_detail_widget_styles.dart b/lib/features/email/presentation/styles/event_attendee_detail_widget_styles.dart new file mode 100644 index 0000000000..f510184b20 --- /dev/null +++ b/lib/features/email/presentation/styles/event_attendee_detail_widget_styles.dart @@ -0,0 +1,11 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventAttendeeDetailWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const double fieldTopPadding = 8; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_attendee_information_widget_styles.dart b/lib/features/email/presentation/styles/event_attendee_information_widget_styles.dart new file mode 100644 index 0000000000..f76346d0a2 --- /dev/null +++ b/lib/features/email/presentation/styles/event_attendee_information_widget_styles.dart @@ -0,0 +1,11 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventAttendeeInformationWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; + static const Color valueOrganizerColor = AppColor.colorOrganizerMailto; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_description_detail_widget_styles.dart b/lib/features/email/presentation/styles/event_description_detail_widget_styles.dart new file mode 100644 index 0000000000..82b3591bd9 --- /dev/null +++ b/lib/features/email/presentation/styles/event_description_detail_widget_styles.dart @@ -0,0 +1,9 @@ + +import 'package:flutter/material.dart'; + +class EventDescriptionDetailWidgetStyles { + static const double textSize = 16; + static const double borderRadius = 16; + static const double contentPadding = 16; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_link_detail_widget_styles.dart b/lib/features/email/presentation/styles/event_link_detail_widget_styles.dart new file mode 100644 index 0000000000..e91c8ba779 --- /dev/null +++ b/lib/features/email/presentation/styles/event_link_detail_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventLinkDetailWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_location_detail_widget_styles.dart b/lib/features/email/presentation/styles/event_location_detail_widget_styles.dart new file mode 100644 index 0000000000..8abe0cdf06 --- /dev/null +++ b/lib/features/email/presentation/styles/event_location_detail_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventLocationDetailWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_location_information_widget_styles.dart b/lib/features/email/presentation/styles/event_location_information_widget_styles.dart new file mode 100644 index 0000000000..c6208b5e5e --- /dev/null +++ b/lib/features/email/presentation/styles/event_location_information_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventLocationInformationWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_time_detail_widget_styles.dart b/lib/features/email/presentation/styles/event_time_detail_widget_styles.dart new file mode 100644 index 0000000000..af54d01924 --- /dev/null +++ b/lib/features/email/presentation/styles/event_time_detail_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventTimeDetailWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_time_information_widget_styles.dart b/lib/features/email/presentation/styles/event_time_information_widget_styles.dart new file mode 100644 index 0000000000..874e389f22 --- /dev/null +++ b/lib/features/email/presentation/styles/event_time_information_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EventTimeInformationWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color labelColor = AppColor.colorSubTitleEventActionText; + static const Color valueColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/event_title_widget_styles.dart b/lib/features/email/presentation/styles/event_title_widget_styles.dart new file mode 100644 index 0000000000..f53abaf925 --- /dev/null +++ b/lib/features/email/presentation/styles/event_title_widget_styles.dart @@ -0,0 +1,7 @@ + +import 'package:flutter/material.dart'; + +class EventTitleWidgetStyles { + static const double textSize = 24; + static const Color textColor = Colors.black; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/organizer_widget_styles.dart b/lib/features/email/presentation/styles/organizer_widget_styles.dart new file mode 100644 index 0000000000..1ea478c4bc --- /dev/null +++ b/lib/features/email/presentation/styles/organizer_widget_styles.dart @@ -0,0 +1,10 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class OrganizerWidgetStyles { + static const double maxWidth = 100; + static const double textSize = 16; + static const Color textColor = Colors.black; + static const Color mailtoColor = AppColor.colorMailto; +} \ No newline at end of file diff --git a/lib/features/email/presentation/styles/see_all_attendees_button_widget_styles.dart b/lib/features/email/presentation/styles/see_all_attendees_button_widget_styles.dart new file mode 100644 index 0000000000..a64f3ce54e --- /dev/null +++ b/lib/features/email/presentation/styles/see_all_attendees_button_widget_styles.dart @@ -0,0 +1,12 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class SeeAllAttendeesButtonWidgetStyles { + static const double textSize = 16; + static const double horizontalPadding = 8; + static const double verticalPadding = 4; + static const double borderRadius = 20; + static const double horizontalMargin = -8; + static const Color textColor = AppColor.primaryColor; +} \ No newline at end of file diff --git a/lib/features/email/presentation/utils/email_utils.dart b/lib/features/email/presentation/utils/email_utils.dart new file mode 100644 index 0000000000..ed0cd8c8b2 --- /dev/null +++ b/lib/features/email/presentation/utils/email_utils.dart @@ -0,0 +1,18 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; + +class EmailUtils { + + static Properties getPropertiesForEmailGetMethod(Session session, AccountId accountId) { + if (CapabilityIdentifier.jamesCalendarEvent.isSupported(session, accountId)) { + return ThreadConstants.propertiesCalendarEvent; + } else { + return ThreadConstants.propertiesDefault; + } + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/app_bar_mail_widget_builder.dart b/lib/features/email/presentation/widgets/app_bar_mail_widget_builder.dart index 3587cc63b9..551846e499 100644 --- a/lib/features/email/presentation/widgets/app_bar_mail_widget_builder.dart +++ b/lib/features/email/presentation/widgets/app_bar_mail_widget_builder.dart @@ -1,9 +1,11 @@ import 'package:core/core.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnBackActionClick = void Function(); @@ -56,21 +58,27 @@ class AppBarMailWidgetBuilder extends StatelessWidget { child: Container( color: Colors.transparent, height: 32, - padding: const EdgeInsets.symmetric(horizontal: 8), + padding: DirectionUtils.isDirectionRTLByLanguage(context) + ? const EdgeInsetsDirectional.only(end: 10) + : const EdgeInsets.symmetric(horizontal: 8), child: Row(mainAxisSize: MainAxisSize.min, children: [ SvgPicture.asset( - _imagePaths.icBack, - width: 14, - height: 14, - color: AppColor.colorTextButton, - fit: BoxFit.fill), + DirectionUtils.isDirectionRTLByLanguage(context) + ? _imagePaths.icArrowRight + : _imagePaths.icBack, + width: DirectionUtils.isDirectionRTLByLanguage(context) ? null : 16, + height: DirectionUtils.isDirectionRTLByLanguage(context) ? null : 16, + colorFilter: AppColor.colorTextButton.asFilter(), + fit: BoxFit.fill), if (!isSearchIsRunning) Container( - margin: const EdgeInsets.only(left: 8), + margin: DirectionUtils.isDirectionRTLByLanguage(context) + ? null + : const EdgeInsetsDirectional.only(start: 8), constraints: BoxConstraints( maxWidth: _responsiveUtils.getSizeScreenWidth(context) - 250), child: Text( - mailboxContain?.name?.name.capitalizeFirstEach ?? '', + mailboxContain?.getDisplayName(context) ?? '', maxLines: 1, overflow: CommonTextStyle.defaultTextOverFlow, softWrap: CommonTextStyle.defaultSoftWrap, @@ -88,7 +96,7 @@ class AppBarMailWidgetBuilder extends StatelessWidget { } bool _supportDisplayMailboxNameTitle(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return _responsiveUtils.isDesktop(context) || _responsiveUtils.isMobile(context) || _responsiveUtils.isTablet(context) || @@ -134,11 +142,11 @@ class AppBarMailWidgetBuilder extends StatelessWidget { buildIconWeb( icon: SvgPicture.asset( _imagePaths.icDeleteComposer, - color: mailboxContain?.isTrash == false - ? AppColor.colorTextButton - : AppColor.colorDeletePermanentlyButton, - width: BuildUtils.isWeb ? 18 : 20, - height: BuildUtils.isWeb ? 18 : 20, + colorFilter: mailboxContain?.isTrash == false + ? AppColor.colorTextButton.asFilter() + : AppColor.colorDeletePermanentlyButton.asFilter(), + width: PlatformInfo.isWeb ? 18 : 20, + height: PlatformInfo.isWeb ? 18 : 20, fit: BoxFit.fill), tooltip: mailboxContain?.isTrash == false ? AppLocalizations.of(context).move_to_trash diff --git a/lib/features/email/presentation/widgets/attachment_file_tile_builder.dart b/lib/features/email/presentation/widgets/attachment_file_tile_builder.dart deleted file mode 100644 index 40fd785310..0000000000 --- a/lib/features/email/presentation/widgets/attachment_file_tile_builder.dart +++ /dev/null @@ -1,105 +0,0 @@ - -import 'package:core/core.dart'; -import 'package:filesize/filesize.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; - -typedef OnDownloadAttachmentFileActionClick = void Function(Attachment attachment); -typedef OnExpandAttachmentActionClick = void Function(); - -class AttachmentFileTileBuilder extends StatelessWidget{ - - final Attachment _attachment; - final OnDownloadAttachmentFileActionClick? onDownloadAttachmentFileActionClick; - - const AttachmentFileTileBuilder( - this._attachment, { - Key? key, - this.onDownloadAttachmentFileActionClick, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final imagePaths = Get.find(); - final responsiveUtils = Get.find(); - - return Padding( - padding: const EdgeInsets.only(right: 12), - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => onDownloadAttachmentFileActionClick?.call(_attachment), - customBorder: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColor.attachmentFileBorderColor), - color: Colors.transparent), - width: responsiveUtils.isMobile(context) ? 224 : 250, - height: 60, - child: Stack( - children: [ - Positioned.fill( - child: Row(children: [ - SvgPicture.asset( - _attachment.getIcon(imagePaths), - width: 44, - height: 44, - fit: BoxFit.fill), - const SizedBox(width: 8), - Expanded(child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _attachment.name ?? '', - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - style: const TextStyle( - fontSize: 14, - color: AppColor.attachmentFileNameColor, - fontWeight: FontWeight.normal), - ), - const SizedBox(height: 4), - Text( - filesize(_attachment.size?.value), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - style: const TextStyle( - fontSize: 12, - color: AppColor.attachmentFileSizeColor, - fontWeight: FontWeight.normal), - ) - ] - )) - ]), - ), - Align( - alignment: Alignment.bottomRight, - child: Material( - color: Colors.transparent, - child: InkWell( - customBorder: const CircleBorder(), - child: SvgPicture.asset( - imagePaths.icDownloadAttachment, - width: 24, - height: 24, - color: AppColor.primaryColor, - fit: BoxFit.fill), - onTap: () => onDownloadAttachmentFileActionClick?.call(_attachment) - ), - ), - ), - ] - ) - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/attachment_item_widget.dart b/lib/features/email/presentation/widgets/attachment_item_widget.dart new file mode 100644 index 0000000000..8150eb2382 --- /dev/null +++ b/lib/features/email/presentation/widgets/attachment_item_widget.dart @@ -0,0 +1,118 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:extended_text/extended_text.dart'; +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/attachment/attachment_item_widget_style.dart'; + +typedef OnDownloadAttachmentFileActionClick = void Function(Attachment attachment); + +class AttachmentItemWidget extends StatelessWidget { + + final Attachment attachment; + final OnDownloadAttachmentFileActionClick? downloadAttachmentAction; + + final _imagePaths = Get.find(); + final _responsiveUtils = Get.find(); + + AttachmentItemWidget({ + Key? key, + required this.attachment, + this.downloadAttachmentAction, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: AttachmentItemWidgetStyle.padding, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => downloadAttachmentAction?.call(attachment), + customBorder: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(AttachmentItemWidgetStyle.radius)) + ), + child: Container( + padding: AttachmentItemWidgetStyle.contentPadding, + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(AttachmentItemWidgetStyle.radius)), + border: Border.all(color: AttachmentItemWidgetStyle.borderColor), + ), + width: _responsiveUtils.isMobile(context) + ? AttachmentItemWidgetStyle.mobileWidth + : AttachmentItemWidgetStyle.width, + height: AttachmentItemWidgetStyle.height, + child: Stack( + children: [ + Positioned.fill( + child: Row(children: [ + SvgPicture.asset( + attachment.getIcon(_imagePaths), + width: AttachmentItemWidgetStyle.iconSize, + height: AttachmentItemWidgetStyle.iconSize, + fit: BoxFit.fill + ), + const SizedBox(width: AttachmentItemWidgetStyle.space), + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ExtendedText( + (attachment.name ?? ''), + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + overflowWidget: TextOverflowWidget( + position: Directionality.maybeOf(context) == TextDirection.rtl + ? TextOverflowPosition.start + : TextOverflowPosition.end, + child: const Text( + "...", + style: AttachmentItemWidgetStyle.dotsLabelTextStyle, + ), + ), + style: AttachmentItemWidgetStyle.labelTextStyle, + ), + const SizedBox(height: 4), + Text( + filesize(attachment.size?.value), + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: AttachmentItemWidgetStyle.sizeLabelTextStyle, + ) + ] + )) + ]), + ), + Align( + alignment: AlignmentDirectional.bottomEnd, + child: Material( + color: Colors.transparent, + child: InkWell( + customBorder: const CircleBorder(), + child: SvgPicture.asset( + _imagePaths.icDownloadAttachment, + width: AttachmentItemWidgetStyle.downloadIconSize, + height: AttachmentItemWidgetStyle.downloadIconSize, + colorFilter: AttachmentItemWidgetStyle.downloadIconColor.asFilter(), + fit: BoxFit.fill + ), + onTap: () => downloadAttachmentAction?.call(attachment) + ), + ), + ), + ] + ) + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart b/lib/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart index 535a80ec78..d8a0c78dc4 100644 --- a/lib/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart +++ b/lib/features/email/presentation/widgets/bottom_bar_mail_widget_builder.dart @@ -1,10 +1,18 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:model/model.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -typedef OnPressEmailActionClick = void Function(EmailActionType emailActionType); +typedef OnPressEmailActionClick = void Function( + EmailActionType emailActionType, + PresentationEmail presentationEmail +); class BottomBarMailWidgetBuilder extends StatelessWidget { @@ -27,69 +35,96 @@ class BottomBarMailWidgetBuilder extends StatelessWidget { final responsiveUtils = Get.find(); return Container( - alignment: Alignment.center, - decoration: responsiveUtils.isWebDesktop(context) - ? const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.only( - bottomRight: Radius.circular(20), - bottomLeft: Radius.circular(20))) - : const BoxDecoration(color: Colors.white), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, + decoration: const BoxDecoration( + border: Border( + top: BorderSide( + color: AppColor.colorDividerHorizontal, + width: 0.5, + ) + ), + ), + child: IntrinsicHeight( + child: Row( children: [ if (_presentationEmail.numberOfAllEmailAddress() > 1) - Expanded(child: (ButtonBuilder(imagePaths.icReplyAll) - ..key(const Key('button_reply_all_message')) - ..size(15) - ..radiusSplash(0) - ..padding(const EdgeInsets.only(top: 5, bottom: 8)) - ..textStyle(TextStyle( + Expanded( + child: TMailButtonWidget( + key: const Key('reply_all_emails_button'), + text: AppLocalizations.of(context).reply_all, + icon: imagePaths.icReplyAll, + borderRadius: 0, + iconSize: 15, + textAlign: TextAlign.center, + flexibleText: true, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + backgroundColor: Colors.transparent, + textStyle: TextStyle( fontSize: responsiveUtils.isPortraitMobile(context) ? 12 : 16, - color: AppColor.colorTextButton)) - ..onPressActionClick(() =>onPressEmailActionClick?.call(EmailActionType.replyAll)) - ..text(AppLocalizations.of(context).reply_all, - isVertical: responsiveUtils.isPortraitMobile(context))) - .build()), - Expanded(child: (ButtonBuilder(imagePaths.icReply) - ..key(const Key('button_reply_message')) - ..size(20) - ..radiusSplash(0) - ..padding(const EdgeInsets.only(top: 5, bottom: 8)) - ..textStyle(TextStyle( + color: AppColor.colorTextButton + ), + verticalDirection: responsiveUtils.isPortraitMobile(context), + onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.replyAll, _presentationEmail), + ), + ), + Expanded( + child: TMailButtonWidget( + key: const Key('reply_email_button'), + text: AppLocalizations.of(context).reply, + icon: imagePaths.icReply, + borderRadius: 0, + iconSize: 20, + textAlign: TextAlign.center, + flexibleText: true, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + backgroundColor: Colors.transparent, + textStyle: TextStyle( fontSize: responsiveUtils.isPortraitMobile(context) ? 12 : 16, - color: AppColor.colorTextButton)) - ..onPressActionClick(() => onPressEmailActionClick?.call(EmailActionType.reply)) - ..text(AppLocalizations.of(context).reply, - isVertical: responsiveUtils.isPortraitMobile(context))) - .build()), - Expanded(child: (ButtonBuilder(imagePaths.icForward) - ..key(const Key('button_forward_message')) - ..size(20) - ..radiusSplash(0) - ..padding(const EdgeInsets.only(top: 5, bottom: 8)) - ..textStyle(TextStyle( + color: AppColor.colorTextButton + ), + verticalDirection: responsiveUtils.isPortraitMobile(context), + onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.reply, _presentationEmail), + ), + ), + Expanded( + child: TMailButtonWidget( + key: const Key('forward_email_button'), + text: AppLocalizations.of(context).forward, + icon: imagePaths.icForward, + borderRadius: 0, + iconSize: 20, + textAlign: TextAlign.center, + flexibleText: true, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + backgroundColor: Colors.transparent, + textStyle: TextStyle( fontSize: responsiveUtils.isPortraitMobile(context) ? 12 : 16, - color: AppColor.colorTextButton)) - ..onPressActionClick(() => onPressEmailActionClick?.call(EmailActionType.forward)) - ..text(AppLocalizations.of(context).forward, - isVertical: responsiveUtils.isPortraitMobile(context))) - .build()), - if (responsiveUtils.mailboxDashboardOnlyHasEmailView(context)) - Expanded(child: (ButtonBuilder(imagePaths.icNewMessage) - ..key(const Key('button_new_message')) - ..size(20) - ..radiusSplash(0) - ..padding(const EdgeInsets.only(top: 5, bottom: 8)) - ..textStyle(TextStyle( - fontSize: responsiveUtils.isPortraitMobile(context) ? 12 : 16, - color: AppColor.colorTextButton)) - ..onPressActionClick(() => onPressEmailActionClick?.call(EmailActionType.compose)) - ..text(AppLocalizations.of(context).new_message, - isVertical: responsiveUtils.isPortraitMobile(context))) - .build()) + color: AppColor.colorTextButton + ), + verticalDirection: responsiveUtils.isPortraitMobile(context), + onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.forward, _presentationEmail), + ), + ), + Expanded( + child: TMailButtonWidget( + key: const Key('compose_new_email_button'), + text: AppLocalizations.of(context).new_message, + icon: imagePaths.icNewMessage, + borderRadius: 0, + iconSize: 20, + textAlign: TextAlign.center, + flexibleText: true, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + backgroundColor: Colors.transparent, + textStyle: TextStyle( + fontSize: responsiveUtils.isPortraitMobile(context) ? 12 : 16, + color: AppColor.colorTextButton + ), + verticalDirection: responsiveUtils.isPortraitMobile(context), + onTapActionCallback: () => onPressEmailActionClick?.call(EmailActionType.compose, _presentationEmail), + ), + ), ] + ), ) ); } diff --git a/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart b/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart new file mode 100644 index 0000000000..607e2a1353 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/attendee_widget.dart @@ -0,0 +1,44 @@ + +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/attendee_widget_styles.dart'; + +class AttendeeWidget extends StatelessWidget { + + final CalendarAttendee attendee; + final List listAttendees; + + const AttendeeWidget({ + super.key, + required this.attendee, + required this.listAttendees, + }); + + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan( + style: const TextStyle( + fontSize: AttendeeWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: AttendeeWidgetStyles.textColor + ), + children: [ + if (attendee.name?.name.isNotEmpty == true) + TextSpan(text: attendee.name!.name), + if (attendee.mailto?.mailAddress.value.isNotEmpty == true) + TextSpan( + text: ' <${attendee.mailto!.mailAddress.value}> ', + style: const TextStyle( + color: AttendeeWidgetStyles.mailtoColor, + fontSize: AttendeeWidgetStyles.textSize, + fontWeight: FontWeight.w500 + ), + ), + if (listAttendees.last != attendee) + const TextSpan(text: ', '), + ] + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_date_icon_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_date_icon_widget.dart new file mode 100644 index 0000000000..ffe82b4203 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_date_icon_widget.dart @@ -0,0 +1,93 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/calendar_date_icon_widget_styles.dart'; + +class CalendarDateIconWidget extends StatelessWidget { + + final CalendarEvent calendarEvent; + final double width; + + const CalendarDateIconWidget({ + super.key, + required this.calendarEvent, + this.width = 100 + }); + + @override + Widget build(BuildContext context) { + return Container( + clipBehavior: Clip.antiAlias, + width: width, + margin: const EdgeInsets.all(CalendarIconWidgetStyles.margin), + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(CalendarIconWidgetStyles.borderRadius))), + color: Colors.white, + shadows: [ + BoxShadow( + color: AppColor.colorShadowBgContentEmail, + blurRadius: 80, + offset: Offset(0, 1), + spreadRadius: 0, + ), + BoxShadow( + color: AppColor.colorShadowCalendarDateIcon, + blurRadius: 3, + offset: Offset(0, 1), + spreadRadius: 1, + ) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + color: AppColor.primaryColor, + padding: const EdgeInsetsDirectional.symmetric( + vertical: CalendarIconWidgetStyles.headerVerticalContentPadding, + horizontal: CalendarIconWidgetStyles.headerHorizontalContentPadding + ), + width: width, + child: Text( + calendarEvent.monthStartDateAsString, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: CalendarIconWidgetStyles.headerTextSize, + fontWeight: FontWeight.w400, + color: Colors.white + ), + ), + ), + Padding( + padding: const EdgeInsets.all(CalendarIconWidgetStyles.bodyContentPadding), + child: Text( + calendarEvent.dayStartDateAsString, + style: const TextStyle( + fontSize: CalendarIconWidgetStyles.bodyDayTextSize, + fontWeight: FontWeight.w700, + color: Colors.black + ), + ), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: CalendarIconWidgetStyles.bodyContentPadding), + child: Divider(color: AppColor.colorCalendarEventInformationStroke, height: 2) + ), + Padding( + padding: const EdgeInsets.all(CalendarIconWidgetStyles.bodyContentPadding), + child: Text( + calendarEvent.weekDayStartDateAsString, + style: const TextStyle( + fontSize: CalendarIconWidgetStyles.bodyWeekDayTextSize, + fontWeight: FontWeight.w400, + color: Colors.black + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart new file mode 100644 index 0000000000..6517341f28 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_banner_widget.dart @@ -0,0 +1,91 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/calendar_event_action_banner_styles.dart'; + +class CalendarEventActionBannerWidget extends StatelessWidget { + + final CalendarEvent calendarEvent; + final List listEmailAddressSender; + + const CalendarEventActionBannerWidget({ + super.key, + required this.calendarEvent, + required this.listEmailAddressSender, + }); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(CalendarEventActionBannerStyles.borderRadius)), + color: calendarEvent.getColorEventActionBanner(listEmailAddressSender).withOpacity(0.12) + ), + padding: const EdgeInsets.all(CalendarEventActionBannerStyles.contentPadding), + margin: const EdgeInsets.symmetric( + horizontal: CalendarEventActionBannerStyles.viewHorizontalMargin, + vertical: CalendarEventActionBannerStyles.viewVerticalMargin, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (calendarEvent.getIconEventAction(imagePaths).isNotEmpty) + Padding( + padding: const EdgeInsetsDirectional.only(end: 8), + child: SvgPicture.asset( + calendarEvent.getIconEventAction(imagePaths), + width: CalendarEventActionBannerStyles.iconSize, + height: CalendarEventActionBannerStyles.iconSize, + fit: BoxFit.fill, + ), + ), + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + RichText( + text: TextSpan( + style: TextStyle( + fontSize: CalendarEventActionBannerStyles.titleTextSize, + fontWeight: FontWeight.w400, + color: calendarEvent.getColorEventActionText(listEmailAddressSender) + ), + children: [ + TextSpan( + text: calendarEvent.getUserNameEventAction( + context: context, + imagePaths: imagePaths, + listEmailAddressSender: listEmailAddressSender + ), + style: TextStyle( + color: calendarEvent.getColorEventActionText(listEmailAddressSender), + fontSize: CalendarEventActionBannerStyles.titleTextSize, + fontWeight: FontWeight.w700 + ), + ), + TextSpan(text: calendarEvent.getTitleEventAction(context, listEmailAddressSender)) + ] + ) + ), + if (calendarEvent.getSubTitleEventAction(context).isNotEmpty) + Text( + calendarEvent.getSubTitleEventAction(context), + style: const TextStyle( + color: AppColor.colorSubTitleEventActionText, + fontSize: CalendarEventActionBannerStyles.subTileTextSize, + fontWeight: FontWeight.w400 + ), + ) + ] + )) + ] + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart new file mode 100644 index 0000000000..4288fad817 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart @@ -0,0 +1,56 @@ + +import 'package:core/core.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/calendar_event_action_button_widget_styles.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; + +class CalendarEventActionButtonWidget extends StatelessWidget { + + final List eventActions; + final EdgeInsetsGeometry? margin; + + const CalendarEventActionButtonWidget({ + super.key, + required this.eventActions, + this.margin, + }); + + @override + Widget build(BuildContext context) { + final responsiveUtils = Get.find(); + return Container( + width: double.infinity, + margin: margin ?? CalendarEventActionButtonWidgetStyles.margin, + padding: responsiveUtils.isPortraitMobile(context) + ? CalendarEventActionButtonWidgetStyles.paddingMobile + : CalendarEventActionButtonWidgetStyles.paddingWeb, + child: Wrap( + spacing: CalendarEventActionButtonWidgetStyles.space, + runSpacing: CalendarEventActionButtonWidgetStyles.space, + children: eventActions + .map((action) => TMailButtonWidget( + text: action.actionType.getLabelButton(context), + backgroundColor: CalendarEventActionButtonWidgetStyles.backgroundColor, + borderRadius: CalendarEventActionButtonWidgetStyles.borderRadius, + padding: CalendarEventActionButtonWidgetStyles.buttonPadding, + textStyle: const TextStyle( + fontWeight: CalendarEventActionButtonWidgetStyles.fontWeight, + fontSize: CalendarEventActionButtonWidgetStyles.textSize, + color: CalendarEventActionButtonWidgetStyles.textColor, + ), + textAlign: TextAlign.center, + minWidth: CalendarEventActionButtonWidgetStyles.minWidth, + width: responsiveUtils.isPortraitMobile(context) ? double.infinity : null, + border: Border.all( + width: CalendarEventActionButtonWidgetStyles.borderWidth, + color: CalendarEventActionButtonWidgetStyles.textColor + ), + onTapActionCallback: () => AppUtils.launchLink(action.link), + )) + .toList(), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart new file mode 100644 index 0000000000..d99f613860 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_detail_widget.dart @@ -0,0 +1,96 @@ + +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/calendar_event_detail_widget_styles.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_attendee_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_description_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_link_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_time_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_title_widget.dart'; + +class CalendarEventDetailWidget extends StatelessWidget { + + final CalendarEvent calendarEvent; + final List eventActions; + final OnOpenNewTabAction? onOpenNewTabAction; + final OnOpenComposerAction? onOpenComposerAction; + + const CalendarEventDetailWidget({ + super.key, + required this.calendarEvent, + required this.eventActions, + this.onOpenNewTabAction, + this.onOpenComposerAction, + }); + + @override + Widget build(BuildContext context) { + return Container( + clipBehavior: Clip.antiAlias, + decoration: const ShapeDecoration( + color: Colors.white, + shape: RoundedRectangleBorder( + side: BorderSide( + width: CalendarEventDetailWidgetStyles.borderStrokeWidth, + color: CalendarEventDetailWidgetStyles.borderStrokeColor, + ), + borderRadius: BorderRadius.all(Radius.circular(CalendarEventDetailWidgetStyles.borderRadius)), + ), + ), + margin: const EdgeInsetsDirectional.symmetric( + vertical: CalendarEventDetailWidgetStyles.verticalMargin, + horizontal: CalendarEventDetailWidgetStyles.horizontalMargin), + padding: const EdgeInsets.all(CalendarEventDetailWidgetStyles.contentPadding), + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (calendarEvent.title?.isNotEmpty == true) + EventTitleWidget(title: calendarEvent.title!), + if (calendarEvent.description?.isNotEmpty == true) + Padding( + padding: const EdgeInsets.only(top: CalendarEventDetailWidgetStyles.fieldTopPadding), + child: EventDescriptionDetailWidget( + description: calendarEvent.description!, + onOpenComposerAction: onOpenComposerAction, + onOpenNewTabAction: onOpenNewTabAction, + ) + ), + if (calendarEvent.dateTimeEventAsString.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: CalendarEventDetailWidgetStyles.fieldTopPadding), + child: EventTimeWidgetWidget(timeEvent: calendarEvent.dateTimeEventAsString), + ), + if (calendarEvent.videoConferences.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: CalendarEventDetailWidgetStyles.fieldTopPadding), + child: EventLinkDetailWidget(listHyperLink: calendarEvent.videoConferences), + ), + if (calendarEvent.location?.isNotEmpty == true) + Padding( + padding: const EdgeInsets.only(top: CalendarEventDetailWidgetStyles.fieldTopPadding), + child: EventLocationDetailWidget( + locationEvent: calendarEvent.location!, + onOpenComposerAction: onOpenComposerAction, + onOpenNewTabAction: onOpenNewTabAction, + ), + ), + if (calendarEvent.participants?.isNotEmpty == true && calendarEvent.organizer != null) + Padding( + padding: const EdgeInsets.only(top: CalendarEventDetailWidgetStyles.fieldTopPadding), + child: EventAttendeeDetailWidget( + attendees: calendarEvent.participants!, + organizer: calendarEvent.organizer!, + ), + ), + if (eventActions.isNotEmpty) + CalendarEventActionButtonWidget(eventActions: eventActions), + ], + ), + ); + } +} diff --git a/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart new file mode 100644 index 0000000000..a2b1d0f444 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/calendar_event_information_widget.dart @@ -0,0 +1,205 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/calendar_event.dart'; +import 'package:tmail_ui_user/features/email/domain/model/event_action.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/calendar_event_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/calendar_event_information_widget_styles.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_event_action_button_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/calendar_date_icon_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_location_information_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_title_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class CalendarEventInformationWidget extends StatelessWidget { + + final CalendarEvent calendarEvent; + final List eventActions; + final OnOpenNewTabAction? onOpenNewTabAction; + final OnOpenComposerAction? onOpenComposerAction; + + const CalendarEventInformationWidget({ + super.key, + required this.calendarEvent, + required this.eventActions, + this.onOpenNewTabAction, + this.onOpenComposerAction, + }); + + @override + Widget build(BuildContext context) { + final responsiveUtils = Get.find(); + return Container( + clipBehavior: Clip.antiAlias, + decoration: const ShapeDecoration( + color: AppColor.colorCalendarEventInformationBackground, + shape: RoundedRectangleBorder( + side: BorderSide( + width: 0.5, + color: AppColor.colorCalendarEventInformationStroke, + ), + borderRadius: BorderRadius.all(Radius.circular(CalendarEventInformationWidgetStyles.borderRadius)), + ), + ), + margin: const EdgeInsetsDirectional.symmetric( + vertical: CalendarEventInformationWidgetStyles.verticalMargin, + horizontal: CalendarEventInformationWidgetStyles.horizontalMargin), + child: responsiveUtils.isPortraitMobile(context) + ? Column( + children: [ + CalendarDateIconWidget( + calendarEvent: calendarEvent, + width: double.infinity, + ), + Container( + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(CalendarEventInformationWidgetStyles.borderRadius), + bottomRight: Radius.circular(CalendarEventInformationWidgetStyles.borderRadius) + ) + ), + color: Colors.white + ), + clipBehavior: Clip.antiAlias, + padding: const EdgeInsets.all(CalendarEventInformationWidgetStyles.calendarInformationMargin), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: const TextStyle( + fontSize: CalendarEventInformationWidgetStyles.invitationMessageTextSize, + fontWeight: FontWeight.w500, + color: CalendarEventInformationWidgetStyles.invitationMessageColor + ), + children: [ + TextSpan( + text: calendarEvent.organizerName, + style: const TextStyle( + color: CalendarEventInformationWidgetStyles.invitationMessageColor, + fontSize: CalendarEventInformationWidgetStyles.invitationMessageTextSize, + fontWeight: FontWeight.w700 + ), + ), + TextSpan(text: AppLocalizations.of(context).invitationMessageCalendarInformation) + ] + ) + ), + const SizedBox(height: CalendarEventInformationWidgetStyles.space), + if (calendarEvent.title?.isNotEmpty == true) + EventTitleWidget(title: calendarEvent.title!), + if (calendarEvent.dateTimeEventAsString.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), + child: EventTimeInformationWidget(timeEvent: calendarEvent.dateTimeEventAsString), + ), + if (calendarEvent.location?.isNotEmpty == true) + Padding( + padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), + child: EventLocationInformationWidget( + locationEvent: calendarEvent.location!, + onOpenComposerAction: onOpenComposerAction, + onOpenNewTabAction: onOpenNewTabAction, + ), + ), + if (calendarEvent.participants?.isNotEmpty == true && calendarEvent.organizer != null) + Padding( + padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), + child: EventAttendeeInformationWidget( + attendees: calendarEvent.participants!, + organizer: calendarEvent.organizer!, + ), + ), + if (eventActions.isNotEmpty) + CalendarEventActionButtonWidget( + eventActions: eventActions, + margin: EdgeInsetsDirectional.zero, + ), + ], + ), + ) + ], + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CalendarDateIconWidget(calendarEvent: calendarEvent), + Expanded(child: Container( + clipBehavior: Clip.antiAlias, + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(CalendarEventInformationWidgetStyles.borderRadius), + bottomRight: Radius.circular(CalendarEventInformationWidgetStyles.borderRadius) + ) + ), + color: Colors.white + ), + padding: const EdgeInsets.all(CalendarEventInformationWidgetStyles.calendarInformationMargin), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + style: const TextStyle( + fontSize: CalendarEventInformationWidgetStyles.invitationMessageTextSize, + fontWeight: FontWeight.w500, + color: CalendarEventInformationWidgetStyles.invitationMessageColor + ), + children: [ + TextSpan( + text: calendarEvent.organizerName, + style: const TextStyle( + color: CalendarEventInformationWidgetStyles.invitationMessageColor, + fontSize: CalendarEventInformationWidgetStyles.invitationMessageTextSize, + fontWeight: FontWeight.w700 + ), + ), + TextSpan(text: AppLocalizations.of(context).invitationMessageCalendarInformation) + ] + ) + ), + const SizedBox(height: CalendarEventInformationWidgetStyles.space), + if (calendarEvent.title?.isNotEmpty == true) + EventTitleWidget(title: calendarEvent.title!), + if (calendarEvent.dateTimeEventAsString.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), + child: EventTimeInformationWidget(timeEvent: calendarEvent.dateTimeEventAsString), + ), + if (calendarEvent.location?.isNotEmpty == true) + Padding( + padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), + child: EventLocationInformationWidget( + locationEvent: calendarEvent.location!, + onOpenComposerAction: onOpenComposerAction, + onOpenNewTabAction: onOpenNewTabAction, + ), + ), + if (calendarEvent.participants?.isNotEmpty == true && calendarEvent.organizer != null) + Padding( + padding: const EdgeInsets.only(top: CalendarEventInformationWidgetStyles.fieldTopPadding), + child: EventAttendeeInformationWidget( + attendees: calendarEvent.participants!, + organizer: calendarEvent.organizer!, + ), + ), + if (eventActions.isNotEmpty) + CalendarEventActionButtonWidget( + eventActions: eventActions, + margin: EdgeInsetsDirectional.zero, + ), + ], + ), + )) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_attendee_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_attendee_detail_widget.dart new file mode 100644 index 0000000000..8a83c240f3 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_attendee_detail_widget.dart @@ -0,0 +1,90 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/list_attendee_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_attendee_detail_widget_styles.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/attendee_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/organizer_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/see_all_attendees_button_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventAttendeeDetailWidget extends StatefulWidget { + + static const int maxAttendeeDisplayed = 6; + + final List attendees; + final CalendarOrganizer organizer; + + const EventAttendeeDetailWidget({ + super.key, + required this.attendees, + required this.organizer + }); + + @override + State createState() => _EventAttendeeDetailWidgetState(); +} + +class _EventAttendeeDetailWidgetState extends State { + + late List _attendeesDisplayed; + late bool _isShowAllAttendee; + + @override + void initState() { + super.initState(); + _attendeesDisplayed = _splitAttendees(widget.attendees); + _isShowAllAttendee = widget.attendees.length <= EventAttendeeDetailWidget.maxAttendeeDisplayed; + log('_EventAttendeeDetailWidgetState::initState:attendees: ${widget.attendees.length} | _isShowAllAttendee: $_isShowAllAttendee'); + } + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventAttendeeDetailWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).attendees, + style: const TextStyle( + fontSize: EventAttendeeDetailWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventAttendeeDetailWidgetStyles.labelColor + ), + ), + ), + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OrganizerWidget(organizer: widget.organizer), + ..._attendeesDisplayed + .map((attendee) => AttendeeWidget(attendee: attendee, listAttendees: _attendeesDisplayed)) + .toList(), + if (!_isShowAllAttendee) + Padding( + padding: const EdgeInsets.only(top: EventAttendeeDetailWidgetStyles.fieldTopPadding), + child: SeeAllAttendeesButtonWidget( + onTap: () { + setState(() { + _attendeesDisplayed = widget.attendees.withoutOrganizer(widget.organizer); + _isShowAllAttendee = true; + }); + } + ), + ) + ] + )) + ], + ); + } + + List _splitAttendees(List attendees) { + final attendeesWithoutOrganizer = attendees.withoutOrganizer(widget.organizer); + return attendeesWithoutOrganizer.length > EventAttendeeDetailWidget.maxAttendeeDisplayed + ? attendeesWithoutOrganizer.sublist(0, EventAttendeeDetailWidget.maxAttendeeDisplayed - 1) + : attendeesWithoutOrganizer; + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart new file mode 100644 index 0000000000..f38edc797a --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_attendee_information_widget.dart @@ -0,0 +1,67 @@ + +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/attendee/calendar_attendee.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/list_attendee_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_attendee_information_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventAttendeeInformationWidget extends StatelessWidget { + + final List attendees; + final CalendarOrganizer organizer; + + const EventAttendeeInformationWidget({ + super.key, + required this.attendees, + required this.organizer + }); + + @override + Widget build(BuildContext context) { + final responsiveUtils = Get.find(); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventAttendeeInformationWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).who, + style: const TextStyle( + fontSize: EventAttendeeInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventAttendeeInformationWidgetStyles.labelColor + ), + ), + ), + Expanded(child: RichText( + text: TextSpan( + style: const TextStyle( + fontSize: EventAttendeeInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventAttendeeInformationWidgetStyles.valueColor + ), + children: [ + TextSpan( + text: '${organizer.mailto?.value} (${AppLocalizations.of(context).organizer})', + style: const TextStyle( + color: EventAttendeeInformationWidgetStyles.valueOrganizerColor, + fontSize: EventAttendeeInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500 + ), + ), + const TextSpan(text: ', '), + TextSpan(text: attendees.withoutOrganizer(organizer).mailtoAsString) + ] + ), + overflow: responsiveUtils.isPortraitMobile(context) + ? TextOverflow.clip + : TextOverflow.ellipsis, + maxLines: responsiveUtils.isPortraitMobile(context) ? null : 2, + )) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_description_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_description_detail_widget.dart new file mode 100644 index 0000000000..5e84e0afbf --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_description_detail_widget.dart @@ -0,0 +1,72 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_description_detail_widget_styles.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart'; + +class EventDescriptionDetailWidget extends StatelessWidget { + + final String description; + final OnOpenNewTabAction? onOpenNewTabAction; + final OnOpenComposerAction? onOpenComposerAction; + + const EventDescriptionDetailWidget({ + super.key, + required this.description, + this.onOpenNewTabAction, + this.onOpenComposerAction, + }); + + @override + Widget build(BuildContext context) { + final imagePath = Get.find(); + return Container( + clipBehavior: Clip.antiAlias, + decoration: const BoxDecoration( + color: AppColor.colorEventDescriptionBackground, + borderRadius: BorderRadius.all(Radius.circular(EventDescriptionDetailWidgetStyles.borderRadius)), + ), + width: double.infinity, + padding: const EdgeInsetsDirectional.all(EventDescriptionDetailWidgetStyles.contentPadding), + child: Stack( + children: [ + Linkify( + onOpen: (element) { + log('EventDescriptionDetailWidget::build:element: $element'); + if (element is UrlElement) { + onOpenNewTabAction?.call(element.url); + } else if (element is EmailElement) { + onOpenComposerAction?.call(element.emailAddress); + } + }, + text: description, + linkifiers: const [ + EmailLinkifier(), + UrlLinkifier() + ], + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: EventDescriptionDetailWidgetStyles.textSize, + color: EventDescriptionDetailWidgetStyles.valueColor + ), + options: const LinkifyOptions( + removeWww: true, + looseUrl: true, + defaultToHttps: true + ), + ), + PositionedDirectional( + top: 0, + end: 0, + child: SvgPicture.asset(imagePath.icFormatQuote) + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_link_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_link_detail_widget.dart new file mode 100644 index 0000000000..32bbccd945 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_link_detail_widget.dart @@ -0,0 +1,39 @@ + +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/widget/hyper_link_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_link_detail_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventLinkDetailWidget extends StatelessWidget { + + final List listHyperLink; + + const EventLinkDetailWidget({ + super.key, + required this.listHyperLink + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventLinkDetailWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).link, + style: const TextStyle( + fontSize: EventLinkDetailWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventLinkDetailWidgetStyles.labelColor + ), + ), + ), + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: listHyperLink.map((link) => HyperLinkWidget(urlString: link)).toList(), + )) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart new file mode 100644 index 0000000000..36d79e84a5 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart @@ -0,0 +1,68 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_location_detail_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnOpenNewTabAction = void Function(String link); +typedef OnOpenComposerAction = void Function(String emailAddress); + +class EventLocationDetailWidget extends StatelessWidget { + + final String locationEvent; + final OnOpenNewTabAction? onOpenNewTabAction; + final OnOpenComposerAction? onOpenComposerAction; + + const EventLocationDetailWidget({ + super.key, + required this.locationEvent, + this.onOpenNewTabAction, + this.onOpenComposerAction, + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventLocationDetailWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).location, + style: const TextStyle( + fontSize: EventLocationDetailWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventLocationDetailWidgetStyles.labelColor + ), + ), + ), + Expanded(child: Linkify( + onOpen: (element) { + log('EventLocationDetailWidget::build:element: $element'); + if (element is UrlElement) { + onOpenNewTabAction?.call(element.url); + } else if (element is EmailElement) { + onOpenComposerAction?.call(element.emailAddress); + } + }, + text: locationEvent, + linkifiers: const [ + EmailLinkifier(), + UrlLinkifier() + ], + style: const TextStyle( + fontSize: EventLocationDetailWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventLocationDetailWidgetStyles.valueColor + ), + options: const LinkifyOptions( + removeWww: true, + looseUrl: true, + defaultToHttps: true + ), + )) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_location_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_location_information_widget.dart new file mode 100644 index 0000000000..5574e99c49 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_location_information_widget.dart @@ -0,0 +1,66 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_linkify/flutter_linkify.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_location_information_widget_styles.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/calendar_event/event_location_detail_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventLocationInformationWidget extends StatelessWidget { + + final String locationEvent; + final OnOpenNewTabAction? onOpenNewTabAction; + final OnOpenComposerAction? onOpenComposerAction; + + const EventLocationInformationWidget({ + super.key, + required this.locationEvent, + this.onOpenNewTabAction, + this.onOpenComposerAction, + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventLocationInformationWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).where, + style: const TextStyle( + fontSize: EventLocationInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventLocationInformationWidgetStyles.labelColor + ), + ), + ), + Expanded(child: Linkify( + onOpen: (element) { + log('EventLocationInformationWidget::build:element: $element'); + if (element is UrlElement) { + onOpenNewTabAction?.call(element.url); + } else if (element is EmailElement) { + onOpenComposerAction?.call(element.emailAddress); + } + }, + text: locationEvent, + linkifiers: const [ + EmailLinkifier(), + UrlLinkifier() + ], + style: const TextStyle( + fontSize: EventLocationInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventLocationInformationWidgetStyles.valueColor + ), + options: const LinkifyOptions( + removeWww: true, + looseUrl: true, + defaultToHttps: true + ), + )) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_time_detail_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_time_detail_widget.dart new file mode 100644 index 0000000000..1af9a6ca94 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_time_detail_widget.dart @@ -0,0 +1,42 @@ + +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_time_detail_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventTimeWidgetWidget extends StatelessWidget { + + final String timeEvent; + + const EventTimeWidgetWidget({ + super.key, + required this.timeEvent + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventTimeDetailWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).time, + style: const TextStyle( + fontSize: EventTimeDetailWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventTimeDetailWidgetStyles.labelColor + ), + ), + ), + Expanded(child: Text( + timeEvent, + style: const TextStyle( + fontSize: EventTimeDetailWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventTimeDetailWidgetStyles.valueColor + ), + )) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart new file mode 100644 index 0000000000..1130e5a23e --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_time_information_widget.dart @@ -0,0 +1,49 @@ + +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_time_information_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EventTimeInformationWidget extends StatelessWidget { + + final String timeEvent; + + const EventTimeInformationWidget({ + super.key, + required this.timeEvent + }); + + @override + Widget build(BuildContext context) { + final responsiveUtils = Get.find(); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: EventTimeInformationWidgetStyles.maxWidth, + child: Text( + AppLocalizations.of(context).when, + style: const TextStyle( + fontSize: EventTimeInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventTimeInformationWidgetStyles.labelColor + ), + ), + ), + Expanded(child: Text( + timeEvent, + overflow: responsiveUtils.isPortraitMobile(context) ? null : CommonTextStyle.defaultTextOverFlow, + softWrap: responsiveUtils.isPortraitMobile(context) ? null : CommonTextStyle.defaultSoftWrap, + maxLines: responsiveUtils.isPortraitMobile(context) ? null : 1, + style: const TextStyle( + fontSize: EventTimeInformationWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: EventTimeInformationWidgetStyles.valueColor + ), + )) + ], + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/event_title_widget.dart b/lib/features/email/presentation/widgets/calendar_event/event_title_widget.dart new file mode 100644 index 0000000000..99284c95c1 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/event_title_widget.dart @@ -0,0 +1,22 @@ + +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/event_title_widget_styles.dart'; + +class EventTitleWidget extends StatelessWidget { + + final String title; + + const EventTitleWidget({super.key, required this.title}); + + @override + Widget build(BuildContext context) { + return Text( + title, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: EventTitleWidgetStyles.textSize, + color: EventTitleWidgetStyles.textColor + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart b/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart new file mode 100644 index 0000000000..4ac0dce035 --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/organizer_widget.dart @@ -0,0 +1,43 @@ + +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/calendar/properties/calendar_organizer.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/organizer_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class OrganizerWidget extends StatelessWidget { + + final CalendarOrganizer organizer; + + const OrganizerWidget({ + super.key, + required this.organizer + }); + + @override + Widget build(BuildContext context) { + return RichText( + text: TextSpan( + style: const TextStyle( + fontSize: OrganizerWidgetStyles.textSize, + fontWeight: FontWeight.w500, + color: OrganizerWidgetStyles.textColor + ), + children: [ + if (organizer.name?.isNotEmpty == true) + TextSpan(text: organizer.name!), + if (organizer.mailto?.value.isNotEmpty == true) + TextSpan( + text: ' <${organizer.mailto!.value}> ', + style: const TextStyle( + color: OrganizerWidgetStyles.mailtoColor, + fontSize: OrganizerWidgetStyles.textSize, + fontWeight: FontWeight.w500 + ), + ), + TextSpan(text: '(${AppLocalizations.of(context).organizer})'), + const TextSpan(text: ', '), + ] + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/calendar_event/see_all_attendees_button_widget.dart b/lib/features/email/presentation/widgets/calendar_event/see_all_attendees_button_widget.dart new file mode 100644 index 0000000000..78ce4d4dcf --- /dev/null +++ b/lib/features/email/presentation/widgets/calendar_event/see_all_attendees_button_widget.dart @@ -0,0 +1,35 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:tmail_ui_user/features/base/widget/material_text_button.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/see_all_attendees_button_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class SeeAllAttendeesButtonWidget extends StatelessWidget { + + final VoidCallback onTap; + + const SeeAllAttendeesButtonWidget({ + super.key, + required this.onTap + }); + + @override + Widget build(BuildContext context) { + return Transform( + transform: Matrix4.translationValues(SeeAllAttendeesButtonWidgetStyles.horizontalMargin, 0.0, 0.0), + child: MaterialTextButton( + label: AppLocalizations.of(context).seeAllAttendees, + onTap: onTap, + borderRadius: SeeAllAttendeesButtonWidgetStyles.borderRadius, + padding: const EdgeInsetsDirectional.symmetric( + horizontal: SeeAllAttendeesButtonWidgetStyles.horizontalPadding, + vertical: SeeAllAttendeesButtonWidgetStyles.verticalPadding + ), + customStyle: const TextStyle( + fontSize: SeeAllAttendeesButtonWidgetStyles.textSize, + color: SeeAllAttendeesButtonWidgetStyles.textColor + ), + ), + ); + } +} diff --git a/lib/features/email/presentation/widgets/draggable_attachment_item_widget.dart b/lib/features/email/presentation/widgets/draggable_attachment_item_widget.dart new file mode 100644 index 0000000000..745392dffd --- /dev/null +++ b/lib/features/email/presentation/widgets/draggable_attachment_item_widget.dart @@ -0,0 +1,38 @@ + +import 'package:flutter/material.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_item_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/feedback_draggable_attachment_item_widget.dart'; + +typedef OnDragAttachmentStarted = Function(); +typedef OnDragAttachmentEnd = Function(DraggableDetails details); + +class DraggableAttachmentItemWidget extends StatelessWidget{ + + final Attachment attachment; + final OnDragAttachmentStarted? onDragStarted; + final OnDragAttachmentEnd? onDragEnd; + final OnDownloadAttachmentFileActionClick? downloadAttachmentAction; + + const DraggableAttachmentItemWidget({ + Key? key, + required this.attachment, + this.onDragStarted, + this.onDragEnd, + this.downloadAttachmentAction, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Draggable( + data: attachment, + feedback: FeedbackDraggableAttachmentItemWidget(attachment: attachment), + onDragStarted: onDragStarted, + onDragEnd: onDragEnd, + child: AttachmentItemWidget( + attachment: attachment, + downloadAttachmentAction: downloadAttachmentAction + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart b/lib/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart index 7c317fdb76..ae218faee7 100644 --- a/lib/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart +++ b/lib/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart @@ -31,7 +31,7 @@ class EmailActionCupertinoActionSheetActionBuilder extends CupertinoActionSheetA return Container( color: bgColor ?? Colors.white, child: MouseRegion( - cursor: BuildUtils.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( key: key, child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart b/lib/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart index d28da22441..458c7b1eab 100644 --- a/lib/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart +++ b/lib/features/email/presentation/widgets/email_address_bottom_sheet_builder.dart @@ -7,6 +7,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/model.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; typedef OnCloseBottomSheetAction = void Function(); typedef OnCopyEmailAddressBottomSheetAction = void Function(EmailAddress); @@ -79,8 +80,9 @@ class EmailAddressBottomSheetBuilder { padding: const EdgeInsets.only(top: 16, right: 16), onPressed: () => _onCloseBottomSheetAction?.call(), icon: SvgPicture.asset( - _imagePaths.icCloseMailbox, - width: 24, height: 24, + _imagePaths.icCircleClose, + width: 24, + height: 24, fit: BoxFit.fill))), (AvatarBuilder() ..text(_emailAddress.asString().firstLetterToUpperCase) @@ -108,22 +110,30 @@ class EmailAddressBottomSheetBuilder { fontWeight: FontWeight.w600, color: AppColor.colorNameEmail), )), - Padding( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: _emailAddress.displayName.isNotEmpty ? 12 : 16), - child: Text( - _emailAddress.emailAddress, - textAlign: TextAlign.center, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - maxLines: 2, - style: const TextStyle( + Material( + color: Colors.transparent, + child: InkWell( + onTap: () {}, + onLongPress: () { + AppUtils.copyEmailAddressToClipboard(_context, _emailAddress.emailAddress); + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 16, vertical: 8), + child: Text( + _emailAddress.emailAddress, + textAlign: TextAlign.center, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + maxLines: 2, + style: const TextStyle( fontSize: 17, fontWeight: FontWeight.normal, color: AppColor.colorMessageConfirmDialog), - )), + ) + ), + ), + ), Material( borderRadius: BorderRadius.circular(20), color: Colors.transparent, diff --git a/lib/features/email/presentation/widgets/email_address_dialog_builder.dart b/lib/features/email/presentation/widgets/email_address_dialog_builder.dart index 3ef759c704..7b9487ea41 100644 --- a/lib/features/email/presentation/widgets/email_address_dialog_builder.dart +++ b/lib/features/email/presentation/widgets/email_address_dialog_builder.dart @@ -6,6 +6,7 @@ import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; typedef OnCloseDialogAction = void Function(); typedef OnCopyEmailAddressDialogAction = void Function(EmailAddress); @@ -55,7 +56,7 @@ class EmailAddressDialogBuilder extends StatelessWidget { padding: const EdgeInsets.only(top: 16, right: 16), onPressed: () => onCloseDialogAction?.call(), icon: SvgPicture.asset( - imagePaths.icCloseMailbox, + imagePaths.icCircleClose, width: 24, height: 24, fit: BoxFit.fill))), @@ -87,22 +88,29 @@ class EmailAddressDialogBuilder extends StatelessWidget { color: AppColor.colorNameEmail), )) ), - Padding( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: _emailAddress.displayName.isNotEmpty ? 12 : 16), - child: Center(child: Text( - _emailAddress.emailAddress, - textAlign: TextAlign.center, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - maxLines: 2, - style: const TextStyle( + Material( + color: Colors.transparent, + child: InkWell( + onTap: () {}, + onLongPress: () { + AppUtils.copyEmailAddressToClipboard(context, _emailAddress.emailAddress); + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 16, vertical: 8), + child: Text( + _emailAddress.emailAddress, + textAlign: TextAlign.center, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + maxLines: 2, + style: const TextStyle( fontSize: 17, fontWeight: FontWeight.normal, color: AppColor.colorMessageConfirmDialog), - )) + ) + ), + ), ), Padding(padding: const EdgeInsets.symmetric(horizontal: 16), child: Center(child: Material( diff --git a/lib/features/email/presentation/widgets/email_attachments_widget.dart b/lib/features/email/presentation/widgets/email_attachments_widget.dart new file mode 100644 index 0000000000..d979593d16 --- /dev/null +++ b/lib/features/email/presentation/widgets/email_attachments_widget.dart @@ -0,0 +1,150 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:model/email/attachment.dart'; +import 'package:model/extensions/list_attachment_extension.dart'; +import 'package:tmail_ui_user/features/base/widget/custom_scroll_behavior.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/email_attachments_styles.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/attachment_item_widget.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/draggable_attachment_item_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EmailAttachmentsWidget extends StatefulWidget { + + final List attachments; + final OnDragAttachmentStarted? onDragStarted; + final OnDragAttachmentEnd? onDragEnd; + final OnDownloadAttachmentFileActionClick? downloadAttachmentAction; + + const EmailAttachmentsWidget({ + super.key, + required this.attachments, + this.onDragStarted, + this.onDragEnd, + this.downloadAttachmentAction + }); + + @override + State createState() => _EmailAttachmentsWidgetState(); +} + +class _EmailAttachmentsWidgetState extends State { + + final _imagePaths = Get.find(); + + bool _isDisplayAll = false; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EmailAttachmentsStyles.padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Row( + children: [ + const SizedBox(width: EmailAttachmentsStyles.headerSpace), + SvgPicture.asset( + _imagePaths.icAttachment, + width: EmailAttachmentsStyles.headerIconSize, + height: EmailAttachmentsStyles.headerIconSize, + colorFilter: EmailAttachmentsStyles.headerIconColor.asFilter(), + fit: BoxFit.fill + ), + const SizedBox(width: EmailAttachmentsStyles.headerSpace), + Expanded( + child: Text( + AppLocalizations.of(context).titleHeaderAttachment( + widget.attachments.length, + filesize(widget.attachments.totalSize(), 1) + ), + style: const TextStyle( + fontSize: EmailAttachmentsStyles.headerTextSize, + fontWeight: EmailAttachmentsStyles.headerFontWeight, + color: EmailAttachmentsStyles.headerTextColor + ) + ) + ), + const SizedBox(width: EmailAttachmentsStyles.headerSpace), + ] + ) + ), + if (widget.attachments.length > 2) + TMailButtonWidget( + text: _isDisplayAll + ? AppLocalizations.of(context).hide + : AppLocalizations.of(context).showAll, + backgroundColor: Colors.transparent, + borderRadius: EmailAttachmentsStyles.buttonBorderRadius, + padding: EmailAttachmentsStyles.buttonPadding, + textStyle: const TextStyle( + fontSize: EmailAttachmentsStyles.buttonTextSize, + color: EmailAttachmentsStyles.buttonTextColor, + fontWeight: EmailAttachmentsStyles.buttonFontWeight + ), + onTapActionCallback: () => setState(() => _isDisplayAll = !_isDisplayAll), + ) + ], + ), + const SizedBox(height: EmailAttachmentsStyles.marginHeader), + if (_isDisplayAll) + Wrap( + runSpacing: EmailAttachmentsStyles.listSpace, + children: widget.attachments + .map((attachment) { + if (PlatformInfo.isWeb) { + return DraggableAttachmentItemWidget( + attachment: attachment, + onDragStarted: widget.onDragStarted, + onDragEnd: widget.onDragEnd, + downloadAttachmentAction: widget.downloadAttachmentAction, + ); + } else { + return AttachmentItemWidget( + attachment: attachment, + downloadAttachmentAction: widget.downloadAttachmentAction + ); + } + }) + .toList() + ) + else + SizedBox( + height: EmailAttachmentsStyles.listHeight, + child: ScrollConfiguration( + behavior: CustomScrollBehavior(), + child: ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemCount: widget.attachments.length, + itemBuilder: (context, index) { + if (PlatformInfo.isWeb) { + return DraggableAttachmentItemWidget( + attachment: widget.attachments[index], + onDragStarted: widget.onDragStarted, + onDragEnd: widget.onDragEnd, + downloadAttachmentAction: widget.downloadAttachmentAction + ); + } else { + return AttachmentItemWidget( + attachment: widget.attachments[index], + downloadAttachmentAction: widget.downloadAttachmentAction + ); + } + } + ), + ) + ), + ], + ), + ); + } +} diff --git a/lib/features/email/presentation/widgets/email_receiver_builder.dart b/lib/features/email/presentation/widgets/email_receiver_widget.dart similarity index 63% rename from lib/features/email/presentation/widgets/email_receiver_builder.dart rename to lib/features/email/presentation/widgets/email_receiver_widget.dart index ec33f8a802..3465bf8aad 100644 --- a/lib/features/email/presentation/widgets/email_receiver_builder.dart +++ b/lib/features/email/presentation/widgets/email_receiver_widget.dart @@ -3,6 +3,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -14,49 +15,67 @@ import 'package:model/extensions/list_email_address_extension.dart'; import 'package:model/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/features/base/widget/material_text_button.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/prefix_email_address_extension.dart'; -import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; -class EmailReceiverBuilder extends StatelessWidget { +typedef OnPreviewEmailAddressActionCallback = Function(BuildContext context, EmailAddress emailAddress); - static const double _maxSizeFullDisplayEmailAddressArrowDownButton = 30.0; +class EmailReceiverWidget extends StatefulWidget { final PresentationEmail emailSelected; - final SingleEmailController controller; - final ResponsiveUtils responsiveUtils; - final ImagePaths imagePaths; final double maxWidth; + final OnPreviewEmailAddressActionCallback? onPreviewEmailAddressActionCallback; - const EmailReceiverBuilder({ + const EmailReceiverWidget({ Key? key, required this.emailSelected, - required this.controller, - required this.responsiveUtils, - required this.imagePaths, this.maxWidth = 200, + this.onPreviewEmailAddressActionCallback, }) : super(key: key); + @override + State createState() => _EmailReceiverWidgetState(); +} + +class _EmailReceiverWidgetState extends State { + + static const double _maxSizeFullDisplayEmailAddressArrowDownButton = 30.0; + + final _imagePaths = Get.find(); + final _responsiveUtils = Get.find(); + + bool _isDisplayAll = false; + @override Widget build(BuildContext context) { - return Obx(() => Row( + return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Expanded(child: _buildEmailAddressOfReceiver( - context, - emailSelected, - controller.isDisplayFullEmailAddress, - maxWidth + Expanded(child: Padding( + padding: EdgeInsets.only(top: _isDisplayAll + ? DirectionUtils.isDirectionRTLByLanguage(context) ? 3 : 5.5 + : 0), + child: _buildEmailAddressOfReceiver( + context, + widget.emailSelected, + _isDisplayAll, + widget.maxWidth + ), )), - if (controller.isDisplayFullEmailAddress) + if (_isDisplayAll) Padding( - padding: const EdgeInsets.only(top: 8), + padding: EdgeInsets.symmetric( + vertical: DirectionUtils.isDirectionRTLByLanguage(context) ? 0 : 6), child: MaterialTextButton( - onTap: controller.collapseEmailAddress, + padding: DirectionUtils.isDirectionRTLByLanguage(context) + ? const EdgeInsets.symmetric(horizontal: 8, vertical: 4) + : null, + onTap: () => setState(() => _isDisplayAll = false), label: AppLocalizations.of(context).hide, ) ) ] - )); + ); } Widget _buildEmailAddressOfReceiver( @@ -102,7 +121,27 @@ class EmailReceiverBuilder extends StatelessWidget { ] ), ), - _buildArrowDownButton() + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => setState(() => _isDisplayAll = true), + customBorder: const CircleBorder(), + child: Container( + padding: const EdgeInsets.all(5), + color: Colors.transparent, + constraints: const BoxConstraints( + maxHeight: _maxSizeFullDisplayEmailAddressArrowDownButton, + maxWidth: _maxSizeFullDisplayEmailAddressArrowDownButton + ), + child: SvgPicture.asset( + _imagePaths.icChevronDown, + width: 16, + height: 16, + fit: BoxFit.fill + ), + ), + ), + ), ] ); } else { @@ -142,37 +181,29 @@ class EmailReceiverBuilder extends StatelessWidget { PrefixEmailAddress prefixEmailAddress, bool isDisplayFull ) { - return Padding( - padding: const EdgeInsets.only(top: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only(top: 5), - child: Text( - '${prefixEmailAddress.asName(context)}:', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: AppColor.colorEmailAddressFull - ) - ), - ), - if (!isDisplayFull && presentationEmail.numberOfAllEmailAddress() > 1) - _buildListEmailAddressWidget( - context, - prefixEmailAddress.listEmailAddress(presentationEmail), - isDisplayFull - ) - else - Expanded(child: _buildListEmailAddressWidget( - context, - prefixEmailAddress.listEmailAddress(presentationEmail), - isDisplayFull - )) - ] - ), + return Row( + children: [ + Text( + '${prefixEmailAddress.asName(context)}:', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: AppColor.colorEmailAddressFull + ) + ), + if (!isDisplayFull && presentationEmail.numberOfAllEmailAddress() > 1) + _buildListEmailAddressWidget( + context, + prefixEmailAddress.listEmailAddress(presentationEmail), + isDisplayFull + ) + else + Expanded(child: _buildListEmailAddressWidget( + context, + prefixEmailAddress.listEmailAddress(presentationEmail), + isDisplayFull + )) + ] ); } @@ -187,7 +218,10 @@ class EmailReceiverBuilder extends StatelessWidget { label: lastEmailAddress == emailAddress ? emailAddress.asString() : '${emailAddress.asString()},', - onTap: () => controller.openEmailAddressDialog(context, emailAddress), + onTap: () => widget.onPreviewEmailAddressActionCallback?.call(context, emailAddress), + onLongPress: () { + AppUtils.copyEmailAddressToClipboard(context, emailAddress.emailAddress); + }, borderRadius: 8, labelColor: Colors.black, labelSize: 16, @@ -212,36 +246,12 @@ class EmailReceiverBuilder extends StatelessWidget { } double _getMaxWidthEmailAddressDisplayed(BuildContext context, double maxWidth) { - if (responsiveUtils.isPortraitMobile(context)) { + if (_responsiveUtils.isPortraitMobile(context)) { return maxWidth - _maxSizeFullDisplayEmailAddressArrowDownButton; - } else if (responsiveUtils.isWebDesktop(context)) { + } else if (_responsiveUtils.isWebDesktop(context)) { return maxWidth / 2; } else { return maxWidth * 3/4; } } - - Widget _buildArrowDownButton() { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: controller.expandEmailAddress, - customBorder: const CircleBorder(), - child: Container( - padding: const EdgeInsets.all(5), - color: Colors.transparent, - constraints: const BoxConstraints( - maxHeight: _maxSizeFullDisplayEmailAddressArrowDownButton, - maxWidth: _maxSizeFullDisplayEmailAddressArrowDownButton - ), - child: SvgPicture.asset( - imagePaths.icChevronDown, - width: 16, - height: 16, - fit: BoxFit.fill - ), - ), - ), - ); - } } \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/email_sender_builder.dart b/lib/features/email/presentation/widgets/email_sender_builder.dart index 0d523d6f2d..70a399e73f 100644 --- a/lib/features/email/presentation/widgets/email_sender_builder.dart +++ b/lib/features/email/presentation/widgets/email_sender_builder.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:tmail_ui_user/features/base/widget/material_text_button.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; typedef OnOpenEmailAddressDetailAction = Function(BuildContext context, EmailAddress emailAddress); @@ -32,6 +33,9 @@ class EmailSenderBuilder extends StatelessWidget { MaterialTextButton( label: emailAddress.displayName, onTap: () => openEmailAddressDetailAction?.call(context, emailAddress), + onLongPress: () { + AppUtils.copyEmailAddressToClipboard(context, emailAddress.emailAddress); + }, borderRadius: 8, padding: const EdgeInsets.all(3), labelSize: 20, @@ -45,6 +49,9 @@ class EmailSenderBuilder extends StatelessWidget { child: MaterialTextButton( label: '<${emailAddress.emailAddress}>', onTap: () => openEmailAddressDetailAction?.call(context, emailAddress), + onLongPress: () { + AppUtils.copyEmailAddressToClipboard(context, emailAddress.emailAddress); + }, borderRadius: 8, padding: const EdgeInsets.all(3), labelSize: 16, diff --git a/lib/features/email/presentation/widgets/email_subject_widget.dart b/lib/features/email/presentation/widgets/email_subject_widget.dart new file mode 100644 index 0000000000..e7611f9634 --- /dev/null +++ b/lib/features/email/presentation/widgets/email_subject_widget.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/email_subject_styles.dart'; + +class EmailSubjectWidget extends StatelessWidget { + + final PresentationEmail presentationEmail; + + const EmailSubjectWidget({super.key, required this.presentationEmail}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EmailSubjectStyles.padding, + child: SelectableText( + presentationEmail.getEmailTitle(), + maxLines: EmailSubjectStyles.maxLines, + minLines: EmailSubjectStyles.minLines, + cursorColor: EmailSubjectStyles.cursorColor, + style: const TextStyle( + fontSize: EmailSubjectStyles.textSize, + color: EmailSubjectStyles.textColor, + fontWeight: EmailSubjectStyles.fontWeight + ) + ) + ); + } +} diff --git a/lib/features/email/presentation/widgets/email_view_empty_widget.dart b/lib/features/email/presentation/widgets/email_view_empty_widget.dart new file mode 100644 index 0000000000..39f86e4a94 --- /dev/null +++ b/lib/features/email/presentation/widgets/email_view_empty_widget.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/email_view_empty_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EmailViewEmptyWidget extends StatelessWidget { + + const EmailViewEmptyWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + AppLocalizations.of(context).no_mail_selected, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: EmailViewEmptyStyles.textSize, + color: EmailViewEmptyStyles.textColor, + fontWeight: EmailViewEmptyStyles.fontWeight + ) + ), + ); + } +} diff --git a/lib/features/email/presentation/widgets/email_view_loading_bar_widget.dart b/lib/features/email/presentation/widgets/email_view_loading_bar_widget.dart new file mode 100644 index 0000000000..f0054755ae --- /dev/null +++ b/lib/features/email/presentation/widgets/email_view_loading_bar_widget.dart @@ -0,0 +1,31 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/widget/cupertino_loading_widget.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/parse_calendar_event_state.dart'; + +class EmailViewLoadingBarWidget extends StatelessWidget { + + final Either viewState; + + const EmailViewLoadingBarWidget({ + super.key, + required this.viewState, + }); + + @override + Widget build(BuildContext context) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is GetEmailContentLoading || success is ParseCalendarEventLoading) { + return const CupertinoLoadingWidget(); + } else { + return const SizedBox.shrink(); + } + } + ); + } +} diff --git a/lib/features/email/presentation/widgets/feedback_draggable_attachment_item_widget.dart b/lib/features/email/presentation/widgets/feedback_draggable_attachment_item_widget.dart new file mode 100644 index 0000000000..08a04a43ab --- /dev/null +++ b/lib/features/email/presentation/widgets/feedback_draggable_attachment_item_widget.dart @@ -0,0 +1,65 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:extended_text/extended_text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:model/email/attachment.dart'; +import 'package:tmail_ui_user/features/email/presentation/extensions/attachment_extension.dart'; +import 'package:tmail_ui_user/features/email/presentation/styles/attachment/feedback_draggable_attachment_item_widget_style.dart'; + +class FeedbackDraggableAttachmentItemWidget extends StatelessWidget { + + final Attachment attachment; + + final _imagePaths = Get.find(); + + FeedbackDraggableAttachmentItemWidget({ + super.key, + required this.attachment + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(FeedbackDraggableAttachmentItemWidgetStyle.radius)), + ), + color: FeedbackDraggableAttachmentItemWidgetStyle.backgroundColor, + shadows: FeedbackDraggableAttachmentItemWidgetStyle.shadows + ), + constraints: const BoxConstraints( + maxWidth: FeedbackDraggableAttachmentItemWidgetStyle.maxWidth + ), + padding: FeedbackDraggableAttachmentItemWidgetStyle.padding, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + attachment.getIcon(_imagePaths), + width: FeedbackDraggableAttachmentItemWidgetStyle.iconSize, + height: FeedbackDraggableAttachmentItemWidgetStyle.iconSize, + fit: BoxFit.fill + ), + const SizedBox(width: FeedbackDraggableAttachmentItemWidgetStyle.space), + Flexible( + child: DefaultTextStyle( + style: FeedbackDraggableAttachmentItemWidgetStyle.labelTextStyle, + child: ExtendedText( + attachment.name ?? '', + maxLines: 1, + overflowWidget: const TextOverflowWidget( + position: TextOverflowPosition.middle, + child: Text( + '...', + style: FeedbackDraggableAttachmentItemWidgetStyle.dotsLabelTextStyle, + ), + ), + ), + ), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart index 4a00793ea3..152187a25c 100644 --- a/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart +++ b/lib/features/email/presentation/widgets/information_sender_and_receiver_builder.dart @@ -6,7 +6,7 @@ import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/presentation_email_extension.dart'; import 'package:tmail_ui_user/features/base/widget/email_avatar_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; -import 'package:tmail_ui_user/features/email/presentation/widgets/email_receiver_builder.dart'; +import 'package:tmail_ui_user/features/email/presentation/widgets/email_receiver_widget.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_sender_builder.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/received_time_builder.dart'; @@ -56,12 +56,10 @@ class InformationSenderAndReceiverBuilder extends StatelessWidget { ReceivedTimeBuilder(emailSelected), ]), if (emailSelected.numberOfAllEmailAddress() > 0) - EmailReceiverBuilder( - controller: controller, + EmailReceiverWidget( emailSelected: emailSelected, - responsiveUtils: responsiveUtils, - imagePaths: imagePaths, maxWidth: constraints.maxWidth, + onPreviewEmailAddressActionCallback: controller.openEmailAddressDialog, ) ] ), diff --git a/lib/features/home/presentation/home_bindings.dart b/lib/features/home/presentation/home_bindings.dart index 1b27ced35b..4cd550ab67 100644 --- a/lib/features/home/presentation/home_bindings.dart +++ b/lib/features/home/presentation/home_bindings.dart @@ -1,7 +1,6 @@ import 'package:core/core.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/cleanup/data/datasource/cleanup_datasource.dart'; import 'package:tmail_ui_user/features/cleanup/data/datasource_impl/cleanup_datasource_impl.dart'; import 'package:tmail_ui_user/features/cleanup/data/local/recent_login_url_cache_manager.dart'; @@ -14,31 +13,11 @@ import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_lo import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_login_username_interactor.dart'; import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_search_cache_interactor.dart'; import 'package:tmail_ui_user/features/home/presentation/home_controller.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/account_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/hive_account_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; -import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; -import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; -import 'package:tmail_ui_user/features/login/data/repository/account_repository_impl.dart'; -import 'package:tmail_ui_user/features/login/data/repository/authentication_oidc_repository_impl.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/get_credential_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_token_oidc_interactor.dart'; import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart'; -import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; -import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; class HomeBindings extends BaseBindings { @@ -48,25 +27,17 @@ class HomeBindings extends BaseBindings { Get.lazyPut(() => HomeController( Get.find(), Get.find(), - Get.find(), - Get.find(tag: BindingTag.isolateTag), Get.find(), Get.find(), Get.find(), Get.find(), Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), )); } @override void bindingsDataSource() { Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); } @override @@ -78,50 +49,24 @@ class HomeBindings extends BaseBindings { Get.find(), Get.find(), )); - Get.lazyPut(() => HiveAccountDatasourceImpl( - Get.find(), - Get.find())); - Get.lazyPut(() => AuthenticationOIDCDataSourceImpl( - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - )); } @override void bindingsInteractor() { - Get.lazyPut(() => GetStoredTokenOidcInteractor( - Get.find(), - Get.find() - )); - Get.lazyPut(() => GetAuthenticatedAccountInteractor( - Get.find(), - Get.find(), - Get.find() - )); Get.lazyPut(() => CleanupEmailCacheInteractor(Get.find())); Get.lazyPut(() => CleanupRecentSearchCacheInteractor(Get.find())); Get.lazyPut(() => CleanupRecentLoginUrlCacheInteractor(Get.find())); Get.lazyPut(() => CleanupRecentLoginUsernameCacheInteractor(Get.find())); - Get.lazyPut(() => DeleteAuthorityOidcInteractor( - Get.find(), - Get.find())); Get.lazyPut(() => CheckOIDCIsAvailableInteractor(Get.find())); } @override void bindingsRepository() { Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); } @override void bindingsRepositoryImpl() { Get.lazyPut(() => CleanupRepositoryImpl(Get.find())); - Get.lazyPut(() => AccountRepositoryImpl(Get.find())); - Get.lazyPut(() => AuthenticationOIDCRepositoryImpl(Get.find())); } } \ No newline at end of file diff --git a/lib/features/home/presentation/home_controller.dart b/lib/features/home/presentation/home_controller.dart index 832546d8ac..814d3fd493 100644 --- a/lib/features/home/presentation/home_controller.dart +++ b/lib/features/home/presentation/home_controller.dart @@ -1,13 +1,18 @@ -import 'package:core/core.dart'; -import 'package:dartz/dartz.dart'; +import 'package:core/data/network/config/dynamic_url_interceptors.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:model/model.dart'; +import 'package:model/account/personal_account.dart'; +import 'package:model/email/email_content.dart'; +import 'package:model/email/email_content_type.dart'; +import 'package:model/oidc/token_oidc.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; -import 'package:tmail_ui_user/features/caching/caching_manager.dart'; +import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; import 'package:tmail_ui_user/features/cleanup/domain/model/cleanup_rule.dart'; import 'package:tmail_ui_user/features/cleanup/domain/model/email_cleanup_rule.dart'; import 'package:tmail_ui_user/features/cleanup/domain/model/recent_login_url_cleanup_rule.dart'; @@ -17,57 +22,39 @@ import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_email_cac import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_login_url_cache_interactor.dart'; import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_login_username_interactor.dart'; import 'package:tmail_ui_user/features/cleanup/domain/usecases/cleanup_recent_search_cache_interactor.dart'; -import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; -import 'package:tmail_ui_user/features/login/domain/state/check_oidc_is_available_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_authenticated_account_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_credential_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_state.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; class HomeController extends BaseController { final GetAuthenticatedAccountInteractor _getAuthenticatedAccountInteractor; final DynamicUrlInterceptors _dynamicUrlInterceptors; - final AuthorizationInterceptors _authorizationInterceptors; - final AuthorizationInterceptors _authorizationIsolateInterceptors; final CleanupEmailCacheInteractor _cleanupEmailCacheInteractor; final EmailReceiveManager _emailReceiveManager; final CleanupRecentSearchCacheInteractor _cleanupRecentSearchCacheInteractor; final CleanupRecentLoginUrlCacheInteractor _cleanupRecentLoginUrlCacheInteractor; final CleanupRecentLoginUsernameCacheInteractor _cleanupRecentLoginUsernameCacheInteractor; - final DeleteCredentialInteractor _deleteCredentialInteractor; - final CachingManager _cachingManager; - final DeleteAuthorityOidcInteractor _deleteAuthorityOidcInteractor; - final CheckOIDCIsAvailableInteractor _checkOIDCIsAvailableInteractor; HomeController( this._getAuthenticatedAccountInteractor, this._dynamicUrlInterceptors, - this._authorizationInterceptors, - this._authorizationIsolateInterceptors, this._cleanupEmailCacheInteractor, this._emailReceiveManager, this._cleanupRecentSearchCacheInteractor, this._cleanupRecentLoginUrlCacheInteractor, this._cleanupRecentLoginUsernameCacheInteractor, - this._deleteCredentialInteractor, - this._cachingManager, - this._deleteAuthorityOidcInteractor, - this._checkOIDCIsAvailableInteractor, ); - Account? currentAccount; + PersonalAccount? currentAccount; @override void onInit() { - log('HomeController::onInit(): '); if (!kIsWeb) { _initFlutterDownloader(); _registerReceivingSharingIntent(); @@ -77,7 +64,6 @@ class HomeController extends BaseController { @override void onReady() { - log('HomeController::onReady()'); _cleanupCache(); super.onReady(); } @@ -91,6 +77,8 @@ class HomeController extends BaseController { static void downloadCallback(String id, DownloadTaskStatus status, int progress) {} void _cleanupCache() async { + await HiveCacheConfig().onUpgradeDatabase(cachingManager); + await Future.wait([ _cleanupEmailCacheInteractor.execute(EmailCleanupRule(Duration.defaultCacheInternal)), _cleanupRecentSearchCacheInteractor.execute(RecentSearchCleanupRule()), @@ -108,12 +96,15 @@ class HomeController extends BaseController { _emailReceiveManager.receivingSharingStream.listen((uri) { log('HomeController::onReady(): Received Email: ${uri.toString()}'); if (uri != null) { - if(GetUtils.isEmail(uri.path)) { + if (GetUtils.isEmail(uri.path)) { log('HomeController::onReady(): Address: ${uri.path}'); _emailReceiveManager.setPendingEmailAddress(EmailAddress(null, uri.path)); - } else { - log('HomeController::onInit(): SharedMediaFilePath: ${uri.path}'); + } else if (uri.scheme == "file") { + log('HomeController::onReady(): SharedMediaFilePath: ${uri.path}'); _emailReceiveManager.setPendingFileInfo([SharedMediaFile(uri.path, null, null, SharedMediaType.FILE)]); + } else { + log('HomeController::onReady(): EmailContent: ${uri.path}'); + _emailReceiveManager.setPendingEmailContent(EmailContent(EmailContentType.textPlain, Uri.decodeComponent(uri.path))); } } }); @@ -125,93 +116,52 @@ class HomeController extends BaseController { } void _goToLogin({LoginArguments? arguments}) { - pushAndPop(AppRoutes.login, arguments: arguments); - } - - @override - void onData(Either newState) { - super.onData(newState); - newState.fold(_handleFailureViewState, _handleSuccessViewState); + popAndPush(AppRoutes.login, arguments: arguments); } @override - void onDone() {} - - @override - void onError(error) { - _clearAllCacheAndCredential(); - } - - void _handleFailureViewState(Failure failure) async { - logError('HomeController::_handleFailureViewState(): ${failure.toString()}'); - if (failure is CheckOIDCIsAvailableFailure) { - _goToLogin(arguments: LoginArguments(LoginFormType.credentialForm)); - } else { - _clearAllCacheAndCredential(); + void handleFailureViewState(Failure failure) async { + super.handleFailureViewState(failure); + if (failure is GetAuthenticatedAccountFailure || + failure is GetStoredTokenOidcFailure || + failure is GetCredentialFailure) { + _goToLogin(arguments: LoginArguments(LoginFormType.baseUrlForm)); } } - void _handleSuccessViewState(Success success) { - log('HomeController::_handleSuccessViewState(): $success'); + @override + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); if (success is GetStoredTokenOidcSuccess) { _goToSessionWithTokenOidc(success); } else if (success is GetCredentialViewState) { _goToSessionWithBasicAuth(success); - } else if (success is CheckOIDCIsAvailableSuccess) { - _goToLogin(arguments: LoginArguments(LoginFormType.ssoForm)); - } - } - - void _clearAllCacheAndCredential() async { - await Future.wait([ - _deleteCredentialInteractor.execute(), - _deleteAuthorityOidcInteractor.execute(), - _cachingManager.clearAll() - ]).then((value) { - if (BuildUtils.isWeb) { - _checkOIDCIsAvailable(); - } else { - _goToLogin(); - } - }); - } - - Uri? _parseUri(String? url) => url != null && url.trim().isNotEmpty - ? Uri.parse(url.trim()) - : null; - - void _checkOIDCIsAvailable() async { - final baseUri = _parseUri(AppConfig.baseUrl); - if (baseUri != null) { - consumeState(_checkOIDCIsAvailableInteractor.execute(OIDCRequest( - baseUrl: baseUri.toString(), - resourceUrl: baseUri.origin))); - } else { - _goToLogin(arguments: LoginArguments(LoginFormType.credentialForm)); } } void _goToSessionWithTokenOidc(GetStoredTokenOidcSuccess storedTokenOidcSuccess) { + _dynamicUrlInterceptors.setJmapUrl(storedTokenOidcSuccess.baseUrl.toString()); _dynamicUrlInterceptors.changeBaseUrl(storedTokenOidcSuccess.baseUrl.toString()); - _authorizationInterceptors.setTokenAndAuthorityOidc( + authorizationInterceptors.setTokenAndAuthorityOidc( newToken: storedTokenOidcSuccess.tokenOidc.toToken(), newConfig: storedTokenOidcSuccess.oidcConfiguration); - _authorizationIsolateInterceptors.setTokenAndAuthorityOidc( + authorizationIsolateInterceptors.setTokenAndAuthorityOidc( newToken: storedTokenOidcSuccess.tokenOidc.toToken(), newConfig: storedTokenOidcSuccess.oidcConfiguration); - pushAndPop(AppRoutes.session, arguments: _dynamicUrlInterceptors.baseUrl); + popAndPush(AppRoutes.session, arguments: _dynamicUrlInterceptors.baseUrl); } void _goToSessionWithBasicAuth(GetCredentialViewState credentialViewState) { + _dynamicUrlInterceptors.setJmapUrl(credentialViewState.baseUrl.origin); _dynamicUrlInterceptors.changeBaseUrl(credentialViewState.baseUrl.origin); - _authorizationInterceptors.setBasicAuthorization( - credentialViewState.userName.userName, + authorizationInterceptors.setBasicAuthorization( + credentialViewState.userName.value, credentialViewState.password.value, ); - _authorizationIsolateInterceptors.setBasicAuthorization( - credentialViewState.userName.userName, + authorizationIsolateInterceptors.setBasicAuthorization( + credentialViewState.userName.value, credentialViewState.password.value, ); - pushAndPop(AppRoutes.session, arguments: _dynamicUrlInterceptors.baseUrl); + popAndPush(AppRoutes.session, arguments: _dynamicUrlInterceptors.baseUrl); } } \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/extesions/size_extension.dart b/lib/features/identity_creator/presentation/extesions/size_extension.dart new file mode 100644 index 0000000000..51dadf3764 --- /dev/null +++ b/lib/features/identity_creator/presentation/extesions/size_extension.dart @@ -0,0 +1,5 @@ + +extension SizeExtension on int { + + int get toBytes => this * 1024; +} \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/identity_creator_bindings.dart b/lib/features/identity_creator/presentation/identity_creator_bindings.dart index 21916ad54b..390d3a85ad 100644 --- a/lib/features/identity_creator/presentation/identity_creator_bindings.dart +++ b/lib/features/identity_creator/presentation/identity_creator_bindings.dart @@ -18,8 +18,4 @@ class IdentityCreatorBindings extends Bindings { Get.find() )); } - - void dispose() { - Get.delete(); - } } \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/identity_creator_controller.dart b/lib/features/identity_creator/presentation/identity_creator_controller.dart index c356d24bc0..61483c8caa 100644 --- a/lib/features/identity_creator/presentation/identity_creator_controller.dart +++ b/lib/features/identity_creator/presentation/identity_creator_controller.dart @@ -1,13 +1,12 @@ - +import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/presentation/utils/keyboard_utils.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; -import 'package:enough_html_editor/enough_html_editor.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/services.dart'; -import 'package:get/get_core/get_core.dart'; -import 'package:get/get_instance/get_instance.dart'; -import 'package:get/get_rx/get_rx.dart'; -import 'package:html_editor_enhanced/html_editor.dart'; +import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; @@ -16,14 +15,16 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -import 'package:model/model.dart'; -import 'package:rich_text_composer/richtext_controller.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:model/extensions/identity_extension.dart'; +import 'package:model/user/user_profile.dart'; +import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_mobile_tablet_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_bindings.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/extesions/size_extension.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/model/identity_creator_arguments.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/model/signature_type.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/utils/identity_creator_constants.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/email_address_validator.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/empty_name_validator.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_view_state.dart'; @@ -33,14 +34,14 @@ import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_id import 'package:tmail_ui_user/features/manage_account/domain/model/edit_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/extensions/identity_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/utils/identity_utils.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:uuid/uuid.dart'; -typedef OnCreatedIdentityCallback = Function(dynamic arguments); - class IdentityCreatorController extends BaseController { final VerifyNameInteractor _verifyNameInteractor; @@ -48,9 +49,9 @@ class IdentityCreatorController extends BaseController { final IdentityUtils _identityUtils; final _uuid = Get.find(); + final _appToast = Get.find(); final noneEmailAddress = EmailAddress(null, 'None'); - final signatureType = SignatureType.plainText.obs; final listEmailAddressDefault = [].obs; final listEmailAddressOfReplyTo = [].obs; final errorNameIdentity = Rxn(); @@ -60,19 +61,18 @@ class IdentityCreatorController extends BaseController { final bccOfIdentity = Rxn(); final actionType = IdentityActionType.create.obs; final isDefaultIdentity = RxBool(false); - final htmlEditorNode = FocusNode(debugLabel: 'html_editor'); - final identityCreatorBinding = IdentityCreatorBindings(); - - late RichTextController keyboardRichTextController; - late RichTextWebController richTextWebController; - late RichTextMobileTabletController richTextMobileTabletController; - late HtmlEditorController signatureHtmlEditorController; - TextEditingController? signaturePlainEditorController; - TextEditingController? inputNameIdentityController; - TextEditingController? inputBccIdentityController; - FocusNode? inputNameIdentityFocusNode; - - HtmlEditorApi? signatureHtmlEditorMobileController; + final isDefaultIdentitySupported = RxBool(false); + final isMobileEditorFocus = RxBool(false); + + final RichTextController keyboardRichTextController = RichTextController(); + final RichTextMobileTabletController richTextMobileTabletController = RichTextMobileTabletController(); + final RichTextWebController richTextWebController = RichTextWebController(); + final TextEditingController inputNameIdentityController = TextEditingController(); + final TextEditingController inputBccIdentityController = TextEditingController(); + final FocusNode inputNameIdentityFocusNode = FocusNode(); + final FocusNode inputBccIdentityFocusNode = FocusNode(); + final ScrollController scrollController = ScrollController(); + String? _nameIdentity; String? _contentHtmlEditor; AccountId? accountId; @@ -80,11 +80,9 @@ class IdentityCreatorController extends BaseController { UserProfile? userProfile; Identity? identity; IdentityCreatorArguments? arguments; - OnCreatedIdentityCallback? onCreatedIdentityCallback; - VoidCallback? onDismissIdentityCreator; - ScrollController? scrollController; final GlobalKey htmlKey = GlobalKey(); + final htmlEditorMinHeight = 150; void updateNameIdentity(BuildContext context, String? value) { _nameIdentity = value; @@ -93,7 +91,13 @@ class IdentityCreatorController extends BaseController { void updateContentHtmlEditor(String? text) => _contentHtmlEditor = text; - String? get contentHtmlEditor => _contentHtmlEditor; + String? get contentHtmlEditor { + if (_contentHtmlEditor != null) { + return _contentHtmlEditor; + } else { + return arguments?.identity?.signatureAsString; + } + } IdentityCreatorController( this._verifyNameInteractor, @@ -104,27 +108,21 @@ class IdentityCreatorController extends BaseController { @override void onInit() { super.onInit(); - keyboardRichTextController = RichTextController(); - richTextWebController = RichTextWebController(); - richTextMobileTabletController = RichTextMobileTabletController(); - signatureHtmlEditorController = - HtmlEditorController(processNewLineAsBr: true); - signaturePlainEditorController = TextEditingController(); - inputNameIdentityController = TextEditingController(); - inputBccIdentityController = TextEditingController(); - inputNameIdentityFocusNode = FocusNode(); - scrollController = ScrollController(); + log('IdentityCreatorController::onInit():arguments: ${Get.arguments}'); + arguments = Get.arguments; } @override void onReady() { super.onReady(); + log('IdentityCreatorController::onReady():'); if (arguments != null) { accountId = arguments!.accountId; session = arguments!.session; userProfile = arguments!.userProfile; identity = arguments!.identity; actionType.value = arguments!.actionType; + _checkDefaultIdentityIsSupported(); _setUpValueFromIdentity(); _getAllIdentities(); } @@ -132,57 +130,58 @@ class IdentityCreatorController extends BaseController { @override void onClose() { + log('IdentityCreatorController::onClose():'); keyboardRichTextController.dispose(); - _disposeWidget(); + inputNameIdentityFocusNode.dispose(); + inputBccIdentityFocusNode.dispose(); + inputNameIdentityController.dispose(); + inputBccIdentityController.dispose(); + scrollController.dispose(); + richTextWebController.onClose(); super.onClose(); } @override - void onDone() { - viewState.value.fold( - (failure) { - if (failure is GetAllIdentitiesFailure) { - _getALlIdentitiesFailure(failure); - } - }, - (success) { - if (success is GetAllIdentitiesSuccess) { - _getALlIdentitiesSuccess(success); - } - } - ); + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetAllIdentitiesSuccess) { + _getALlIdentitiesSuccess(success); + } } - void _setUpValueFromIdentity() { - _nameIdentity = identity?.name ?? ''; - inputNameIdentityController?.text = identity?.name ?? ''; + @override + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is GetAllIdentitiesFailure) { + _getALlIdentitiesFailure(failure); + } + } - if (identity?.textSignature?.value.isNotEmpty == true) { - signaturePlainEditorController?.text = identity?.textSignature?.value ?? ''; + void _checkDefaultIdentityIsSupported() { + if (session != null && accountId != null) { + isDefaultIdentitySupported.value = [CapabilityIdentifier.jamesSortOrder].isSupported(session!, accountId!); } + } - if (identity?.htmlSignature?.value.isNotEmpty == true) { - updateContentHtmlEditor(identity?.htmlSignature?.value ?? ''); - signatureHtmlEditorController.setText(identity?.htmlSignature?.value ?? ''); + void _setUpValueFromIdentity() { + _nameIdentity = identity?.name ?? ''; + inputNameIdentityController.text = identity?.name ?? ''; + + if (identity?.signatureAsString.isNotEmpty == true) { + updateContentHtmlEditor(arguments?.identity?.signatureAsString ?? ''); + if (PlatformInfo.isWeb) { + richTextWebController.editorController.setText(arguments?.identity?.signatureAsString ?? ''); + } } } void _getAllIdentities() { - log('IdentityCreatorController::_getAllIdentities() '); - if (accountId != null) { - try { - requireCapability(session!, accountId!, [CapabilityIdentifier.jamesSortOrder]); - consumeState(_getAllIdentitiesInteractor.execute( - accountId!, - properties: Properties({'email', 'sortOrder'}) - )); - } catch (e) { - logError('IdentityCreatorController::_getAllIdentities(): exception: $e'); - consumeState(_getAllIdentitiesInteractor.execute( - accountId!, - properties: Properties({'email'}) - )); - } + if (accountId != null && session != null) { + final propertiesRequired = isDefaultIdentitySupported.isTrue + ? Properties({'email', 'sortOrder'}) + : Properties({'email'}); + + consumeState(_getAllIdentitiesInteractor.execute(session!, accountId!, properties: propertiesRequired)); } } @@ -195,7 +194,10 @@ class IdentityCreatorController extends BaseController { listEmailAddressOfReplyTo.add(noneEmailAddress); listEmailAddressOfReplyTo.addAll(listEmailAddressDefault); _setUpAllFieldEmailAddress(); - _setUpDefaultIdentity(success.identities); + + if (isDefaultIdentitySupported.isTrue) { + _setUpDefaultIdentity(success.identities); + } } else { _setDefaultEmailAddressList(); } @@ -235,7 +237,7 @@ class IdentityCreatorController extends BaseController { if (identity?.bcc?.isNotEmpty == true) { bccOfIdentity.value = identity?.bcc!.first; - inputBccIdentityController?.text = identity?.bcc!.first.emailAddress ?? ''; + inputBccIdentityController.text = identity?.bcc!.first.emailAddress ?? ''; } else { bccOfIdentity.value = null; } @@ -282,15 +284,6 @@ class IdentityCreatorController extends BaseController { return defaultIdentityIds?.length != allIdentities?.length; } - void selectSignatureType(BuildContext context, SignatureType newSignatureType) async { - if (newSignatureType == SignatureType.plainText && !BuildUtils.isWeb) { - final signatureText = await _getSignatureHtmlText(); - updateContentHtmlEditor(signatureText); - } - clearFocusEditor(context); - signatureType.value = newSignatureType; - } - void updateEmailOfIdentity(EmailAddress? newEmailAddress) { emailOfIdentity.value = newEmailAddress; } @@ -304,10 +297,10 @@ class IdentityCreatorController extends BaseController { } Future _getSignatureHtmlText() async { - if (BuildUtils.isWeb) { - return signatureHtmlEditorController.getText(); + if (PlatformInfo.isWeb) { + return richTextWebController.editorController.getText(); } else { - return signatureHtmlEditorMobileController?.getText(); + return keyboardRichTextController.htmlEditorApi?.getText(); } } @@ -317,17 +310,18 @@ class IdentityCreatorController extends BaseController { final error = _getErrorInputNameString(context); if (error?.isNotEmpty == true) { errorNameIdentity.value = error; + inputNameIdentityFocusNode.requestFocus(); return; } final errorBcc = _getErrorInputAddressString(context); if (errorBcc?.isNotEmpty == true) { errorBccIdentity.value = errorBcc; + inputBccIdentityFocusNode.requestFocus(); return; } - final signaturePlainText = signaturePlainEditorController?.text ?? ''; - final signatureHtmlText = BuildUtils.isWeb + final signatureHtmlText = PlatformInfo.isWeb ? contentHtmlEditor : await _getSignatureHtmlText(); final bccAddress = bccOfIdentity.value != null && bccOfIdentity.value != noneEmailAddress @@ -337,16 +331,15 @@ class IdentityCreatorController extends BaseController { ? {replyToOfIdentity.value!} : {}; - final sortOrder = isDefaultIdentity.value - ? UnsignedInt(0) - : UnsignedInt(100); + final sortOrder = isDefaultIdentitySupported.isTrue + ? UnsignedInt(isDefaultIdentity.value ? 0 : 100) + : null; final newIdentity = Identity( name: _nameIdentity, email: emailOfIdentity.value?.email, replyTo: replyToAddress, bcc: bccAddress, - textSignature: Signature(signaturePlainText), htmlSignature: Signature(signatureHtmlText ?? ''), sortOrder: sortOrder); @@ -356,30 +349,15 @@ class IdentityCreatorController extends BaseController { final identityRequest = CreateNewIdentityRequest( generateCreateId, newIdentity, - isDefaultIdentity: isDefaultIdentity.value - ); - _disposeWidget(); - - if (BuildUtils.isWeb) { - onCreatedIdentityCallback?.call(identityRequest); - } else { - popBack(result: identityRequest); - } - + isDefaultIdentity: isDefaultIdentity.value); + popBack(result: identityRequest); } else { final identityRequest = EditIdentityRequest( - identityId: identity!.id!, - identityRequest: newIdentity.toIdentityRequest(), - isDefaultIdentity: isDefaultIdentity.value); - - if (BuildUtils.isWeb) { - _disposeWidget(); - onCreatedIdentityCallback?.call(identityRequest); - } else { - popBack(result: identityRequest); - } + identityId: identity!.id!, + identityRequest: newIdentity.toIdentityRequest(), + isDefaultIdentity: isDefaultIdentity.value); + popBack(result: identityRequest); } - identityCreatorBinding.dispose(); } void onCheckboxChanged() { @@ -403,7 +381,7 @@ class IdentityCreatorController extends BaseController { } String? _getErrorInputAddressString(BuildContext context, {String? value}) { - final emailAddress = value ?? inputBccIdentityController?.text ?? ''; + final emailAddress = value ?? inputBccIdentityController.text; if (emailAddress.trim().isEmpty) { return null; } @@ -440,58 +418,107 @@ class IdentityCreatorController extends BaseController { } void clearFocusEditor(BuildContext context) { - if (!BuildUtils.isWeb) { - signatureHtmlEditorMobileController?.unfocus(); - richTextMobileTabletController.htmlEditorApi?.unfocus(); - keyboardRichTextController.hideRichTextView(); + if (PlatformInfo.isMobile) { + keyboardRichTextController.htmlEditorApi?.unfocus(); + KeyboardUtils.hideSystemKeyboardMobile(); } - SystemChannels.textInput.invokeMethod('TextInput.hide'); + KeyboardUtils.hideKeyboard(context); } void closeView(BuildContext context) { - if (BuildUtils.isWeb) { - _disposeWidget(); - onDismissIdentityCreator?.call(); - } else { - popBack(); - identityCreatorBinding.dispose(); - } + clearFocusEditor(context); + popBack(); } - void _disposeWidget() { - signaturePlainEditorController?.dispose(); - signaturePlainEditorController = null; - inputNameIdentityFocusNode?.dispose(); - inputNameIdentityFocusNode = null; - inputNameIdentityController?.dispose(); - inputNameIdentityController = null; - inputBccIdentityController?.dispose(); - inputBccIdentityController = null; - scrollController?.dispose(); - scrollController = null; + void initRichTextForMobile(BuildContext context, HtmlEditorApi editorApi) { + richTextMobileTabletController.htmlEditorApi = editorApi; + keyboardRichTextController.onCreateHTMLEditor( + editorApi, + onEnterKeyDown: _onEnterKeyDownOnMobile, + onFocus: _onFocusHTMLEditorOnMobile, + context: context + ); + keyboardRichTextController.htmlEditorApi?.onFocusOut = () { + keyboardRichTextController.hideRichTextView(); + isMobileEditorFocus.value = false; + }; } - void onFocusHTMLEditor() async { - await Scrollable.ensureVisible(htmlKey.currentContext!); + void _onFocusHTMLEditorOnMobile() async { + inputBccIdentityFocusNode.unfocus(); + inputNameIdentityFocusNode.unfocus(); + isMobileEditorFocus.value = true; + if (htmlKey.currentContext != null) { + await Scrollable.ensureVisible(htmlKey.currentContext!); + } await Future.delayed(const Duration(milliseconds: 500), () { - if (scrollController != null) { - scrollController!.animateTo( - scrollController!.position.pixels + defaultKeyboardToolbarHeight, - duration: const Duration(milliseconds: 1), - curve: Curves.linear, - ); - } + final offset = scrollController.position.pixels + + defaultKeyboardToolbarHeight + + htmlEditorMinHeight; + scrollController.animateTo( + offset, + duration: const Duration(milliseconds: 1), + curve: Curves.linear, + ); }); } - void onEnterKeyDown() { - if(scrollController != null && - scrollController!.position.pixels < scrollController!.position.maxScrollExtent) { - scrollController!.animateTo( - scrollController!.position.pixels + 20, + void _onEnterKeyDownOnMobile() { + if (scrollController.position.pixels < scrollController.position.maxScrollExtent) { + scrollController.animateTo( + scrollController.position.pixels + 20, duration: const Duration(milliseconds: 1), curve: Curves.linear, ); } } + + void pickImage(BuildContext context, {int? maxWidth}) async { + clearFocusEditor(context); + + final filePickerResult = await FilePicker.platform.pickFiles( + type: FileType.image, + withData: true + ); + + if (context.mounted) { + final platformFile = filePickerResult?.files.single; + if (platformFile != null) { + _insertInlineImage(context, platformFile, maxWidth: maxWidth); + } else { + _appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).cannotSelectThisImage + ); + } + } else { + logError("IdentityCreatorController::pickImage: context is unmounted"); + } + } + + bool _isExceedMaxSizeInlineImage(int fileSize) => + fileSize > IdentityCreatorConstants.maxKBSizeIdentityInlineImage.toBytes; + + void _insertInlineImage( + BuildContext context, + PlatformFile platformFile, + {int? maxWidth} + ) { + if (_isExceedMaxSizeInlineImage(platformFile.size)) { + _appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).pleaseChooseAnImageSizeCorrectly( + IdentityCreatorConstants.maxKBSizeIdentityInlineImage + ) + ); + } else { + if (PlatformInfo.isWeb) { + richTextWebController.insertImageAsBase64(platformFile: platformFile); + } else if (PlatformInfo.isMobile) { + richTextMobileTabletController.insertImageAsBase64(platformFile: platformFile, maxWidth: maxWidth); + } else { + logError("IdentityCreatorController::_insertInlineImage: Platform not supported"); + } + } + } } \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/identity_creator_view.dart b/lib/features/identity_creator/presentation/identity_creator_view.dart index dc5d17e9ee..c971a4d341 100644 --- a/lib/features/identity_creator/presentation/identity_creator_view.dart +++ b/lib/features/identity_creator/presentation/identity_creator_view.dart @@ -1,20 +1,25 @@ -import 'dart:math'; +import 'dart:math' as math; -import 'package:core/core.dart'; -import 'package:enough_html_editor/enough_html_editor.dart' as html_editor_mobile; +import 'package:core/presentation/extensions/capitalize_extension.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/html_transformer/html_utils.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/presentation/views/responsive/responsive_widget.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:html_editor_enhanced/html_editor.dart' as html_editor_browser; -import 'package:html_editor_enhanced/html_editor.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:rich_text_composer/views/keyboard_richtext.dart'; +import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart'; -import 'package:tmail_ui_user/features/composer/presentation/widgets/toolbar_rich_text_builder.dart'; +import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart'; +import 'package:tmail_ui_user/features/composer/presentation/widgets/web/toolbar_rich_text_builder.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_controller.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/model/identity_creator_arguments.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/model/signature_type.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_field_builder.dart'; @@ -23,8 +28,10 @@ import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/set import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; -class IdentityCreatorView extends GetWidget { +class IdentityCreatorView extends GetWidget + with RichTextButtonMixin { final _imagePaths = Get.find(); final _responsiveUtils = Get.find(); @@ -32,522 +39,437 @@ class IdentityCreatorView extends GetWidget { @override final controller = Get.find(); - IdentityCreatorView({Key? key}) : super(key: key) { - controller.arguments = Get.arguments; - } - - IdentityCreatorView.fromArguments( - IdentityCreatorArguments arguments, { - Key? key, - OnCreatedIdentityCallback? onCreatedIdentityCallback, - VoidCallback? onDismissCallback - }) : super(key: key) { - controller.arguments = arguments; - controller.onCreatedIdentityCallback = onCreatedIdentityCallback; - controller.onDismissIdentityCreator = onDismissCallback; - } + IdentityCreatorView({super.key}); @override Widget build(BuildContext context) { - return ResponsiveWidget( - responsiveUtils: _responsiveUtils, - mobile: GestureDetector( + final responsiveWidget = ResponsiveWidget( + responsiveUtils: _responsiveUtils, + mobile: Scaffold( + backgroundColor: Colors.black38, + body: GestureDetector( onTap: () => controller.clearFocusEditor(context), - child: SizedBox( - height: MediaQuery.of(context).size.height * 0.95, + child: Card( + margin: EdgeInsets.zero, + borderOnForeground: false, + color: Colors.transparent, child: SafeArea( - child: BuildUtils.isWeb ? Scaffold(body: _buildBodyMobile(context)) : _buildBodyMobile(context), - ), - ) - ), - landscapeMobile: Scaffold( - backgroundColor: Colors.white, - body: GestureDetector( - onTap: () => controller.clearFocusEditor(context), - child: SafeArea( - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.zero), - child: _buildBodyMobile(context) + top: PlatformInfo.isMobile, + bottom: false, + left: false, + right: false, + child: ClipRRect( + borderRadius: const BorderRadius.only( + topRight: Radius.circular(16), + topLeft: Radius.circular(16)), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.only( + topRight: Radius.circular(16), + topLeft: Radius.circular(16) + ), + ), + child: _buildBodyView(context), ), ), - ) - ), - tablet: Scaffold( - backgroundColor: Colors.black.withAlpha(24), - body: GestureDetector( - onTap: () => controller.clearFocusEditor(context), - child: Center(child: Card( - color: Colors.transparent, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(16))), - width: _responsiveUtils.getSizeScreenWidth(context) * 0.85, - height: _responsiveUtils.getSizeScreenHeight(context) * 0.6, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16)), - child: _buildBodyMobile(context) - ) - ) - )) ), + ), ), - tabletLarge: Scaffold( - backgroundColor: Colors.black.withAlpha(24), - body: GestureDetector( - onTap: () => controller.clearFocusEditor(context), - child: Center(child: Card( - color: Colors.transparent, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(16))), - width: _responsiveUtils.getSizeScreenWidth(context) * 0.85, - height: _responsiveUtils.getSizeScreenHeight(context) * 0.6, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16)), - child: _buildBodyMobile(context) - ) - ) - )) - ) - ), - landscapeTablet: Scaffold( - backgroundColor: Colors.black.withAlpha(24), - body: GestureDetector( - onTap: () => controller.clearFocusEditor(context), - child: Center(child: Card( - color: Colors.transparent, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(16))), - width: _responsiveUtils.getSizeScreenWidth(context) * 0.65, - height: _responsiveUtils.getSizeScreenHeight(context) * 0.8, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16)), - child: _buildBodyMobile(context) - ) + ), + landscapeMobile: Scaffold( + backgroundColor: Colors.white, + body: GestureDetector( + onTap: () => controller.clearFocusEditor(context), + child: SafeArea( + child: _buildBodyView(context), + ), + ) + ), + tablet: Scaffold( + backgroundColor: Colors.black38, + body: GestureDetector( + onTap: () => controller.clearFocusEditor(context), + child: Center( + child: Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 24), + child: Card( + color: Colors.transparent, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)) + ), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(16)) + ), + width: math.max(_responsiveUtils.getSizeScreenWidth(context) * 0.4, 700), + height: _responsiveUtils.getSizeScreenHeight(context) * 0.8, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: _buildBodyView(context) ) - )) + ) + ), ) + ) ), - desktop: Scaffold( - backgroundColor: Colors.black.withAlpha(24), - body: GestureDetector( - onTap: () => controller.clearFocusEditor(context), - child: Center(child: Card( - color: Colors.transparent, - shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))), - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.all(Radius.circular(16))), - width: max(_responsiveUtils.getSizeScreenWidth(context) * 0.4, 650), - height: _responsiveUtils.getSizeScreenHeight(context) * 0.75, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16)), - child: _buildBodyMobile(context) - ) - ) - )), + ), + desktop: Scaffold( + backgroundColor: Colors.black38, + body: GestureDetector( + onTap: () => controller.clearFocusEditor(context), + child: Center(child: Card( + color: Colors.transparent, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)) + ), + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(16)) + ), + width: math.max(_responsiveUtils.getSizeScreenWidth(context) * 0.4, 800), + height: _responsiveUtils.getSizeScreenHeight(context) * 0.8, + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(16)), + child: _buildBodyView(context) + ) ) + )), ) + ) ); + + if (PlatformInfo.isWeb) { + return responsiveWidget; + } else { + return KeyboardRichText( + keyBroadToolbar: RichTextKeyboardToolBar( + titleBack: AppLocalizations.of(context).titleFormat, + backgroundKeyboardToolBarColor: PlatformInfo.isIOS + ? AppColor.colorBackgroundKeyboard + : AppColor.colorBackgroundKeyboardAndroid, + isLandScapeMode: _responsiveUtils.isLandscapeMobile(context), + richTextController: controller.keyboardRichTextController, + titleQuickStyleBottomSheet: AppLocalizations.of(context).titleQuickStyles, + titleBackgroundBottomSheet: AppLocalizations.of(context).titleBackground, + titleForegroundBottomSheet: AppLocalizations.of(context).titleForeground, + titleFormatBottomSheet: AppLocalizations.of(context).titleFormat, + insertImage: () => controller.pickImage(context, maxWidth: _getMaxWidth(context).toInt()), + ), + richTextController: controller.keyboardRichTextController, + paddingChild: EdgeInsets.zero, + child: responsiveWidget + ); + } } - Widget _buildBodyMobile(BuildContext context) { + Widget _buildBodyView(BuildContext context) { final bodyCreatorView = SingleChildScrollView( controller: controller.scrollController, physics: const ClampingScrollPhysics(), child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column(children: [ - Obx(() => (IdentityInputFieldBuilder( + padding: const EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 24), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Obx(() => IdentityInputFieldBuilder( AppLocalizations.of(context).name, controller.errorNameIdentity.value, - AppLocalizations.of(context).required, + AppLocalizations.of(context).required, editingController: controller.inputNameIdentityController, focusNode: controller.inputNameIdentityFocusNode, - isMandatory: true) - ..addOnChangeInputNameAction((value) => controller.updateNameIdentity(context, value))) - .build()), + isMandatory: true, + onChangeInputNameAction: (value) => controller.updateNameIdentity(context, value) + )), const SizedBox(height: 24), Obx(() { if (controller.actionType.value == IdentityActionType.create) { - return (IdentityDropListFieldBuilder( - _imagePaths, - AppLocalizations.of(context).email.inCaps, - controller.emailOfIdentity.value, - controller.listEmailAddressDefault) - ..addOnSelectEmailAddressDropListAction((emailAddress) => - controller.updateEmailOfIdentity(emailAddress)) - ).build(); + return IdentityDropListFieldBuilder( + _imagePaths, + AppLocalizations.of(context).email.inCaps, + controller.emailOfIdentity.value, + controller.listEmailAddressDefault, + onSelectItemDropList: controller.updateEmailOfIdentity); } else { return IdentityFieldNoEditableBuilder( - AppLocalizations.of(context).email.inCaps, - controller.emailOfIdentity.value - ).build(); + AppLocalizations.of(context).email.inCaps, + controller.emailOfIdentity.value); } }), const SizedBox(height: 24), - Obx(() => (IdentityDropListFieldBuilder( - _imagePaths, - AppLocalizations.of(context).reply_to, - controller.replyToOfIdentity.value, - controller.listEmailAddressOfReplyTo) - ..addOnSelectEmailAddressDropListAction((newEmailAddress) => - controller.updaterReplyToOfIdentity(newEmailAddress))) - .build()), + Obx(() => IdentityDropListFieldBuilder( + _imagePaths, + AppLocalizations.of(context).reply_to, + controller.replyToOfIdentity.value, + controller.listEmailAddressOfReplyTo, + onSelectItemDropList: controller.updaterReplyToOfIdentity + )), const SizedBox(height: 24), - Obx(() => (IdentityInputWithDropListFieldBuilder( - AppLocalizations.of(context).bcc_to, - controller.errorBccIdentity.value, - controller.inputBccIdentityController) - ..addOnSelectedSuggestionAction((newEmailAddress) { - controller.inputBccIdentityController?.text = newEmailAddress?.email ?? ''; + Obx(() => IdentityInputWithDropListFieldBuilder( + AppLocalizations.of(context).bcc_to, + controller.errorBccIdentity.value, + controller.inputBccIdentityController, + focusNode: controller.inputBccIdentityFocusNode, + onSelectedSuggestionAction: (newEmailAddress) { + controller.inputBccIdentityController.text = newEmailAddress?.email ?? ''; controller.updateBccOfIdentity(newEmailAddress); - }) - ..addOnChangeInputSuggestionAction((pattern) { + }, + onChangeInputSuggestionAction: (pattern) { controller.validateInputBccAddress(context, pattern); if (pattern == null || pattern.trim().isEmpty) { controller.updateBccOfIdentity(null); } else { controller.updateBccOfIdentity(EmailAddress(null, pattern)); } - }) - ..addOnSuggestionCallbackAction((pattern) => - controller.getSuggestionEmailAddress(pattern))) - .build() - ), + }, + onSuggestionCallbackAction: controller.getSuggestionEmailAddress + )), const SizedBox(height: 32), - Row( - children: [ - Text(AppLocalizations.of(context).signature, - style: const TextStyle( - fontWeight: FontWeight.normal, - fontSize: 14, - color: AppColor.colorContentEmail, - ), - ), - Expanded( - child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - Obx(() => _buildSignatureButton(context, SignatureType.plainText)), - const SizedBox(width: 10), - Obx(() => _buildSignatureButton(context, SignatureType.htmlTemplate)), - ]), - ) - ], + Text(AppLocalizations.of(context).signature, + style: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 14, + color: AppColor.colorContentEmail, + ), ), const SizedBox(height: 8), - Obx(() => PointerInterceptor( - child: Container( - height: 300, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColor.colorInputBorderCreateMailbox), - color: Colors.white, - ), - child: Stack( - children: [ - if (controller.signatureType.value == SignatureType.plainText) - _buildSignaturePlainTextTemplate(context) - else - _buildSignatureHtmlTemplate(context) - ] - ), + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColor.colorInputBorderCreateMailbox), ), - )), - if (_responsiveUtils.isTablet(context) || _responsiveUtils.isMobile(context))...[ - Obx(() => Padding( - padding: const EdgeInsets.only(top: 27, bottom: 135), - child: SetDefaultIdentityCheckboxBuilder( - imagePaths: _imagePaths, - isCheck: controller.isDefaultIdentity.value, - onCheckboxChanged: controller.onCheckboxChanged), - )), - const SizedBox(height: 24), - Container( - alignment: Alignment.center, - color: Colors.white, - child: Row( - children: [ - Expanded( - child: buildTextButton( - AppLocalizations.of(context).cancel, - textStyle: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 17, - color: AppColor.colorTextButton, - ), - backgroundColor: AppColor.emailAddressChipColor, - width: 128, - height: 44, - radius: 10, - onTap: () => controller.closeView(context), - ), - ), - const SizedBox(width: 12), - Expanded( - child: Obx(() => controller.viewState.value.fold( - (failure) => buildTextButton( - controller.actionType.value == IdentityActionType.create - ? AppLocalizations.of(context).create - : AppLocalizations.of(context).save, - width: 128, - height: 44, - radius: 10, - onTap: () => controller.createNewIdentity(context)), - (success) { - if (success is GetAllIdentitiesLoading) { - return const Center( - key: Key('create_loading_icon'), - child: CircularProgressIndicator(color: AppColor.primaryColor)); - } else { - return buildTextButton( - controller.actionType.value == IdentityActionType.create - ? AppLocalizations.of(context).create - : AppLocalizations.of(context).save, - width: 128, - height: 44, - radius: 10, - onTap: () => controller.createNewIdentity(context)); - } - } - )), - ), - ] - ), - )] else ...[ - _buildActionBottomDesktop(context) - ] + padding: const EdgeInsetsDirectional.all(16), + child: _buildSignatureHtmlTemplate(context), + ), + const SizedBox(height: 12), + if (_isMobile(context)) + _buildActionButtonMobile(context) + else + _buildActionButtonDesktop(context), + if (PlatformInfo.isMobile) + Obx(() { + if (controller.isMobileEditorFocus.isTrue) { + return const SizedBox(height: 48); + } else { + return const SizedBox.shrink(); + } + }) ]), ), ); return GestureDetector( onTap: () => controller.clearFocusEditor(context), - child: Stack( - children: [ - Column(children: [ - Padding( - padding: const EdgeInsets.only(top: 14), - child: Obx(() { - return Text(controller.actionType.value == IdentityActionType.create - ? AppLocalizations.of(context).createNewIdentity.inCaps - : AppLocalizations.of(context).edit_identity.inCaps, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.black)); - })), - const SizedBox(height: 8), - Expanded( - child: BuildUtils.isWeb - ? PointerInterceptor(child: bodyCreatorView) - : KeyboardRichText( - keyBroadToolbar: RichTextKeyboardToolBar( - titleBack: AppLocalizations.of(context).titleFormat, - backgroundKeyboardToolBarColor: AppColor.colorBackgroundKeyboard, - isLandScapeMode: _responsiveUtils.isLandscapeMobile(context), - richTextController: controller.keyboardRichTextController, - titleQuickStyleBottomSheet: AppLocalizations.of(context).titleQuickStyles, - titleBackgroundBottomSheet: AppLocalizations.of(context).titleBackground, - titleForegroundBottomSheet: AppLocalizations.of(context).titleForeground, - titleFormatBottomSheet: AppLocalizations.of(context).titleFormat, - ), - richTextController: controller.keyboardRichTextController, - child: bodyCreatorView), - ), - ]), - Positioned(top: 2, right: 8, - child: buildIconWeb( - iconSize: 24, - icon: SvgPicture.asset(_imagePaths.icComposerClose, fit: BoxFit.fill, color: AppColor.colorDeleteContactIcon), - tooltip: AppLocalizations.of(context).close, - onTap: () => controller.closeView(context))) - ] - ), + child: Column(children: [ + _buildHeaderView(context), + Expanded(child: PointerInterceptor(child: bodyCreatorView)) + ]), ); } - Widget _buildSignatureButton(BuildContext context, SignatureType signatureType) { - return buildButtonWrapText( - signatureType.getTitle(context), - textStyle: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - color: controller.signatureType.value == signatureType - ? AppColor.colorContentEmail - : AppColor.colorHintSearchBar), - bgColor: controller.signatureType.value == signatureType - ? AppColor.emailAddressChipColor - : Colors.transparent, - height: 30, - radius: 10, - onTap: () => controller.selectSignatureType(context, signatureType)); - } - - Widget _buildSignaturePlainTextTemplate(BuildContext context) { - if (BuildUtils.isWeb) { - return SizedBox( - height: 230, - child: (TextFieldBuilder() - ..key(const Key('signature_plain_text_editor')) - ..cursorColor(Colors.black) - ..addController(controller.signaturePlainEditorController) - ..textStyle(const TextStyle( - fontWeight: FontWeight.normal, - color: Colors.black, - fontSize: 16)) - ..maxLines(null)) - .build(), - ); - } else { - return(TextFieldBuilder() - ..key(const Key('signature_plain_text_editor')) - ..cursorColor(Colors.black) - ..addController(controller.signaturePlainEditorController) - ..textStyle(const TextStyle( - fontWeight: FontWeight.normal, - color: Colors.black, - fontSize: 16)) - ..minLines(12) - ..maxLines(null)) - .build(); - } + Widget _buildHeaderView(BuildContext context) { + return Container( + color: Colors.white, + height: 52, + child: Row(children: [ + const SizedBox(width: 40), + Expanded(child: Obx(() { + return Text( + controller.actionType.value == IdentityActionType.create + ? AppLocalizations.of(context).createNewIdentity.inCaps + : AppLocalizations.of(context).edit_identity.inCaps, + maxLines: 1, + textAlign: TextAlign.center, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Colors.black + )); + })), + buildIconWeb( + iconSize: 24, + icon: SvgPicture.asset( + _imagePaths.icComposerClose, + fit: BoxFit.fill, + colorFilter: AppColor.colorDeleteContactIcon.asFilter() + ), + tooltip: AppLocalizations.of(context).close, + onTap: () => controller.closeView(context)), + ]), + ); } Widget _buildSignatureHtmlTemplate(BuildContext context) { - final htmlEditor = BuildUtils.isWeb - ? _buildHtmlEditorWeb(context, controller.contentHtmlEditor ?? '') - : _buildHtmlEditor(context, initialContent: controller.contentHtmlEditor ?? ''); + final htmlEditor = PlatformInfo.isWeb + ? _buildHtmlEditorWeb(context, controller.contentHtmlEditor ?? '') + : _buildHtmlEditor(context, initialContent: controller.contentHtmlEditor ?? ''); - return SizedBox( - height: 300, - child: Column( - children: [ - if(BuildUtils.isWeb) - ToolbarRichTextWebBuilder( - richTextWebController: controller.richTextWebController, - padding: const EdgeInsets.only(top: 22, bottom: 8.0, left: 24, right: 12)), - htmlEditor, - ], - ), + return Column( + children: [ + if (PlatformInfo.isWeb) + ToolbarRichTextWebBuilder( + richTextWebController: controller.richTextWebController, + padding: const EdgeInsets.only(bottom: 12), + extendedOption: [ + Padding( + padding: const EdgeInsetsDirectional.only(end: 4.0), + child: buildWrapIconStyleText( + icon: buildIconWithTooltip( + path: _imagePaths.icAddPicture, + tooltip: AppLocalizations.of(context).insertImage + ), + hasDropdown: false, + onTap: () => controller.pickImage(context) + ), + ), + ] + ), + htmlEditor, + ], ); } Widget _buildHtmlEditorWeb(BuildContext context, String initContent) { - log('IdentityCreatorView::_buildHtmlEditorWeb(): initContent: $initContent'); - return Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 14.0, right: 2.0), - child: html_editor_browser.HtmlEditor( - key: const Key('identity_create_editor_web'), - controller: controller.richTextWebController.editorController, - htmlEditorOptions: const HtmlEditorOptions( - hint: '', - darkMode: false, - customBodyCssStyle: bodyCssStyleForEditor), - blockQuotedContent: initContent, - htmlToolbarOptions: const HtmlToolbarOptions( - toolbarType: ToolbarType.hide, - defaultToolbarButtons: []), - otherOptions: const OtherOptions(height: 550), - callbacks: Callbacks(onBeforeCommand: (currentHtml) { - log('IdentityCreatorView::_buildHtmlEditorWeb(): onBeforeCommand : $currentHtml'); - controller.updateContentHtmlEditor(currentHtml); - }, onChangeContent: (changed) { - log('IdentityCreatorView::_buildHtmlEditorWeb(): onChangeContent : $changed'); - controller.updateContentHtmlEditor(changed); - }, onInit: () { - log('IdentityCreatorView::_buildHtmlEditorWeb(): onInit'); - controller.updateContentHtmlEditor(initContent); - controller.richTextWebController.setFullScreenEditor(); - controller.richTextWebController.setEnableCodeView(); - }, onFocus: () { - log('IdentityCreatorView::_buildHtmlEditorWeb(): onFocus'); - FocusManager.instance.primaryFocus?.unfocus(); - Future.delayed(const Duration(milliseconds: 500), () { - controller.richTextWebController.editorController.setFocus(); - }); - controller.richTextWebController.closeAllMenuPopup(); - }, onChangeSelection: (settings) { - controller.richTextWebController.onEditorSettingsChange(settings); - }, onChangeCodeview: (contentChanged) { - log('IdentityCreatorView::_buildHtmlEditorWeb(): onChangeCodeView : $contentChanged'); - controller.updateContentHtmlEditor(contentChanged); - }), - ), + return html_editor_browser.HtmlEditor( + key: const Key('identity_create_editor_web'), + controller: controller.richTextWebController.editorController, + htmlEditorOptions: html_editor_browser.HtmlEditorOptions( + shouldEnsureVisible: true, + hint: '', + darkMode: false, + initialText: initContent, + customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: AppUtils.getCurrentDirection(context)), + ), + htmlToolbarOptions: const html_editor_browser.HtmlToolbarOptions( + toolbarType: html_editor_browser.ToolbarType.hide, + defaultToolbarButtons: [] + ), + otherOptions: const html_editor_browser.OtherOptions(height: 200), + callbacks: html_editor_browser.Callbacks( + onBeforeCommand: controller.updateContentHtmlEditor, + onChangeContent: controller.updateContentHtmlEditor, + onInit: () { + controller.richTextWebController.editorController.setFullScreen(); + controller.updateContentHtmlEditor(initContent); + }, onFocus: () { + FocusManager.instance.primaryFocus?.unfocus(); + Future.delayed(const Duration(milliseconds: 500), () { + controller.richTextWebController.editorController.setFocus(); + }); + controller.richTextWebController.closeAllMenuPopup(); + }, + onChangeSelection: controller.richTextWebController.onEditorSettingsChange, + onChangeCodeview: controller.updateContentHtmlEditor ), ); } Widget _buildHtmlEditor(BuildContext context, {String? initialContent}) { - final richTextMobileTabletController = controller.richTextMobileTabletController; - return Focus( - focusNode: controller.htmlEditorNode, - child: html_editor_mobile.HtmlEditor( - key: const Key('identity_create_editor'), - minHeight: 111, - addDefaultSelectionMenuItems: false, - initialContent: initialContent ?? '', - onCreated: (editorApi) { - richTextMobileTabletController.htmlEditorApi = editorApi; - controller.keyboardRichTextController.onCreateHTMLEditor( - editorApi, - onEnterKeyDown: controller.onEnterKeyDown, - context: context, - ); - }, - ) + return HtmlEditor( + key: controller.htmlKey, + minHeight: controller.htmlEditorMinHeight, + addDefaultSelectionMenuItems: false, + initialContent: initialContent ?? '', + customStyleCss: HtmlUtils.customCssStyleHtmlEditor(direction: AppUtils.getCurrentDirection(context)), + onCreated: (editorApi) => controller.initRichTextForMobile(context, editorApi), ); } - Widget _buildActionBottomDesktop(BuildContext context) { + Widget _buildActionButtonDesktop(BuildContext context) { return Row( children: [ - Obx(() { - return SetDefaultIdentityCheckboxBuilder( - imagePaths: _imagePaths, - isCheck: controller.isDefaultIdentity.value, - onCheckboxChanged: controller.onCheckboxChanged); - }), - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 24.0, bottom: 12.0, left: 12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Padding( - padding: const EdgeInsets.only(right: 12.0), - child: buildTextButton( - AppLocalizations.of(context).cancel, - textStyle: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 17, - color: AppColor.colorTextButton, - ), - backgroundColor: AppColor.emailAddressChipColor, - width: 156, - height: 44, - radius: 10, - onTap: () => controller.closeView(context), - ), - ), - buildTextButton( - controller.actionType.value == IdentityActionType.create - ? AppLocalizations.of(context).create - : AppLocalizations.of(context).save, - width: 156, - height: 44, - radius: 10, - onTap: () => controller.createNewIdentity(context)), - ], - ), - ), - ) + Expanded(child: _buildCheckboxIdentityDefault(context)), + const SizedBox(width: 12), + _buildCancelButton(context, width: 156), + const SizedBox(width: 12), + _buildSaveButton(context, width: 156) ], ); } + + Widget _buildActionButtonMobile(BuildContext context) { + return Column(children: [ + _buildCheckboxIdentityDefault(context), + const SizedBox(height: 24), + Row(children: [ + Expanded(child: _buildCancelButton(context)), + const SizedBox(width: 12), + Expanded(child: _buildSaveButton(context)) + ]) + ]); + } + + Widget _buildCheckboxIdentityDefault(BuildContext context) { + return Obx(() { + if (controller.isDefaultIdentitySupported.isTrue) { + return SetDefaultIdentityCheckboxBuilder( + imagePaths: _imagePaths, + isCheck: controller.isDefaultIdentity.value, + onCheckboxChanged: controller.onCheckboxChanged); + } else { + return const SizedBox.shrink(); + } + }); + } + + Widget _buildCancelButton(BuildContext context, {double? width}) { + return buildTextButton( + AppLocalizations.of(context).cancel, + textStyle: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 17, + color: AppColor.colorTextButton, + ), + backgroundColor: AppColor.emailAddressChipColor, + width: width ?? 128, + height: 44, + radius: 10, + onTap: () => controller.closeView(context), + ); + } + + Widget _buildSaveButton(BuildContext context, {double? width}) { + return Obx(() => controller.viewState.value.fold( + (failure) => buildTextButton( + controller.actionType.value == IdentityActionType.create + ? AppLocalizations.of(context).create + : AppLocalizations.of(context).save, + width: width ?? 128, + height: 44, + radius: 10, + onTap: () => controller.createNewIdentity(context)), + (success) { + if (success is GetAllIdentitiesLoading) { + return const Center( + key: Key('create_loading_icon'), + child: CircularProgressIndicator(color: AppColor.primaryColor)); + } else { + return buildTextButton( + controller.actionType.value == IdentityActionType.create + ? AppLocalizations.of(context).create + : AppLocalizations.of(context).save, + width: width ?? 128, + height: 44, + radius: 10, + onTap: () => controller.createNewIdentity(context)); + } + } + )); + } + + bool _isMobile(BuildContext context) => + _responsiveUtils.isPortraitMobile(context) || + _responsiveUtils.isLandscapeMobile(context); + + double _getMaxWidth(BuildContext context) { + if (_isMobile(context)) { + return _responsiveUtils.getSizeScreenWidth(context); + } else if (_responsiveUtils.isDesktop(context)) { + return math.max(_responsiveUtils.getSizeScreenWidth(context) * 0.4, 800); + } else { + return math.max(_responsiveUtils.getSizeScreenWidth(context) * 0.4, 700); + } + } } \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/model/signature_type.dart b/lib/features/identity_creator/presentation/model/signature_type.dart deleted file mode 100644 index c56fb0aca2..0000000000 --- a/lib/features/identity_creator/presentation/model/signature_type.dart +++ /dev/null @@ -1,20 +0,0 @@ - -import 'package:flutter/cupertino.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -enum SignatureType { - plainText, - htmlTemplate -} - -extension SignatureTypeExtension on SignatureType { - - String getTitle(BuildContext context) { - switch(this) { - case SignatureType.plainText: - return AppLocalizations.of(context).plain_text; - case SignatureType.htmlTemplate: - return AppLocalizations.of(context).html; - } - } -} \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/utils/identity_creator_constants.dart b/lib/features/identity_creator/presentation/utils/identity_creator_constants.dart new file mode 100644 index 0000000000..e26a3b27ab --- /dev/null +++ b/lib/features/identity_creator/presentation/utils/identity_creator_constants.dart @@ -0,0 +1,4 @@ + +class IdentityCreatorConstants { + static const int maxKBSizeIdentityInlineImage = 16; // Kilobyte +} \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart index 0464a8dd32..ea9fb12dea 100644 --- a/lib/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart +++ b/lib/features/identity_creator/presentation/widgets/identity_drop_list_field_builder.dart @@ -1,47 +1,50 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/style_utils.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; typedef OnSelectEmailAddressDropListAction = Function(EmailAddress? emailAddress); -class IdentityDropListFieldBuilder { +class IdentityDropListFieldBuilder extends StatelessWidget { final ImagePaths _imagePaths; final String _label; final EmailAddress? _emailAddressSelected; final List _listEmailAddress; + final OnSelectEmailAddressDropListAction? onSelectItemDropList; - OnSelectEmailAddressDropListAction? onSelectItemDropList; - - IdentityDropListFieldBuilder( + const IdentityDropListFieldBuilder( this._imagePaths, this._label, this._emailAddressSelected, - this._listEmailAddress, - ); - - void addOnSelectEmailAddressDropListAction(OnSelectEmailAddressDropListAction action) { - onSelectItemDropList = action; - } + this._listEmailAddress, { + super.key, + this.onSelectItemDropList + }); - Widget build() { + @override + Widget build(BuildContext context) { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(_label, style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: AppColor.colorContentEmail)), + Text( + _label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: AppColor.colorContentEmail)), const SizedBox(height: 8), DropdownButtonHideUnderline( child: DropdownButton2( isExpanded: true, - hint: Row( + hint: const Row( children: [ Expanded(child: Text( - _emailAddressSelected?.email ?? '', - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.normal, color: Colors.black), + '', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.normal, color: Colors.black), maxLines: 1, overflow: CommonTextStyle.defaultTextOverFlow, softWrap: CommonTextStyle.defaultSoftWrap, @@ -51,7 +54,7 @@ class IdentityDropListFieldBuilder { items: _listEmailAddress.map((item) => DropdownMenuItem( value: item, child: Text( - item.email ?? '', + item.emailAddress, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.normal, color: Colors.black), maxLines: 1, overflow: CommonTextStyle.defaultTextOverFlow, @@ -59,24 +62,35 @@ class IdentityDropListFieldBuilder { ), )).toList(), value: _emailAddressSelected, - onChanged: (newEmailAddress) => onSelectItemDropList?.call(newEmailAddress), - icon: SvgPicture.asset(_imagePaths.icDropDown), - buttonPadding: const EdgeInsets.symmetric(horizontal: 12), - buttonDecoration: BoxDecoration( + onChanged: onSelectItemDropList, + buttonStyleData: ButtonStyleData( + height: 44, + padding: const EdgeInsetsDirectional.only(end: 8), + decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), border: Border.all(color: AppColor.colorInputBorderCreateMailbox, width: 0.5), color: AppColor.colorInputBackgroundCreateMailbox), - itemHeight: 44, - buttonHeight: 44, - selectedItemHighlightColor: Colors.black12, - itemPadding: const EdgeInsets.symmetric(horizontal: 12), - dropdownMaxHeight: 200, - dropdownDecoration: BoxDecoration( + ), + dropdownStyleData: DropdownStyleData( + maxHeight: 200, + decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Colors.white), - dropdownElevation: 4, - scrollbarRadius: const Radius.circular(40), - scrollbarThickness: 6, + elevation: 4, + scrollbarTheme: ScrollbarThemeData( + radius: const Radius.circular(40), + thickness: MaterialStateProperty.all(6), + thumbVisibility: MaterialStateProperty.all(true), + ), + ), + iconStyleData: IconStyleData( + icon: SvgPicture.asset(_imagePaths.icDropDown), + iconSize: 14, + ), + menuItemStyleData: const MenuItemStyleData( + height: 44, + padding: EdgeInsets.symmetric(horizontal: 12), + ), ), ) ]); diff --git a/lib/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart index 9bd004f05e..7feac9e2da 100644 --- a/lib/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart +++ b/lib/features/identity_creator/presentation/widgets/identity_field_no_editable_builder.dart @@ -1,40 +1,49 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/style_utils.dart'; import 'package:flutter/material.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; -class IdentityFieldNoEditableBuilder { +class IdentityFieldNoEditableBuilder extends StatelessWidget { final String _label; final EmailAddress? _emailAddressSelected; - IdentityFieldNoEditableBuilder(this._label, this._emailAddressSelected); + const IdentityFieldNoEditableBuilder( + this._label, + this._emailAddressSelected, + {super.key} + ); - Widget build() { + @override + Widget build(BuildContext context) { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(_label, style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: AppColor.colorContentEmail)), + Text( + _label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: AppColor.colorContentEmail)), const SizedBox(height: 8), Container( - height: 44, - alignment: Alignment.centerLeft, - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColor.colorInputBorderCreateMailbox, width: 0.5), - color: AppColor.colorInputBackgroundCreateMailbox), - child: Text( - _emailAddressSelected?.email ?? '', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - color: AppColor.colorInputBorderCreateMailbox), - maxLines: 1, - overflow: BuildUtils.isWeb ? null : TextOverflow.ellipsis, - ) + height: 44, + alignment: Alignment.centerLeft, + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColor.colorInputBorderCreateMailbox, width: 0.5), + color: AppColor.colorInputBackgroundCreateMailbox), + child: Text( + _emailAddressSelected?.email ?? '', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: AppColor.colorInputBorderCreateMailbox), + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap + ) ), ]); } diff --git a/lib/features/identity_creator/presentation/widgets/identity_input_field_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_input_field_builder.dart index 873045cf3a..edf550a2d3 100644 --- a/lib/features/identity_creator/presentation/widgets/identity_input_field_builder.dart +++ b/lib/features/identity_creator/presentation/widgets/identity_input_field_builder.dart @@ -1,11 +1,14 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/text/text_field_builder.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/widgets/identity_input_decoration_builder.dart'; typedef OnChangeInputNameAction = Function(String? value); -class IdentityInputFieldBuilder { +class IdentityInputFieldBuilder extends StatelessWidget { final String _label; final String? _error; @@ -14,24 +17,22 @@ class IdentityInputFieldBuilder { final FocusNode? focusNode; final TextInputType? inputType; final bool isMandatory; + final OnChangeInputNameAction? onChangeInputNameAction; - OnChangeInputNameAction? onChangeInputNameAction; - - IdentityInputFieldBuilder( + const IdentityInputFieldBuilder( this._label, this._error, this.requiredIndicator, { + super.key, this.isMandatory = false, this.editingController, this.focusNode, - this.inputType + this.inputType, + this.onChangeInputNameAction }); - void addOnChangeInputNameAction(OnChangeInputNameAction action) { - onChangeInputNameAction = action; - } - - Widget build() { + @override + Widget build(BuildContext context) { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( isMandatory @@ -41,21 +42,23 @@ class IdentityInputFieldBuilder { fontWeight: FontWeight.w500, color: AppColor.colorContentEmail)), const SizedBox(height: 8), - (TextFieldBuilder() - ..onChange((value) => onChangeInputNameAction?.call(value)) - ..textInputAction(TextInputAction.next) - ..addController(editingController ?? TextEditingController()) - ..autoFocus(true) - ..addFocusNode(focusNode) - ..textStyle(const TextStyle(color: Colors.black, fontSize: 16)) - ..keyboardType(inputType ?? TextInputType.text) - ..textDecoration((IdentityInputDecorationBuilder() - ..setContentPadding(const EdgeInsets.symmetric( - vertical: BuildUtils.isWeb ? 16 : 12, - horizontal: 12)) - ..setErrorText(_error)) - .build())) - .build() + TextFieldBuilder( + onTextChange: onChangeInputNameAction, + textInputAction: TextInputAction.next, + autoFocus: true, + maxLines: 1, + textDirection: DirectionUtils.getDirectionByLanguage(context), + controller: editingController, + focusNode: focusNode, + textStyle: const TextStyle(color: Colors.black, fontSize: 16), + keyboardType: inputType ?? TextInputType.text, + decoration: (IdentityInputDecorationBuilder() + ..setContentPadding(const EdgeInsets.symmetric( + vertical: PlatformInfo.isWeb ? 16 : 12, + horizontal: 12)) + ..setErrorText(_error)) + .build(), + ) ]); } } \ No newline at end of file diff --git a/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart b/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart index e597f26c64..edfd81f2d2 100644 --- a/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart +++ b/lib/features/identity_creator/presentation/widgets/identity_input_with_drop_list_field_builder.dart @@ -1,5 +1,7 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; @@ -9,59 +11,54 @@ typedef OnSelectedSuggestionAction = Function(EmailAddress? emailAddress); typedef OnSuggestionCallbackAction = Function(String? pattern); typedef OnChangeInputSuggestionAction = Function(String? pattern); -class IdentityInputWithDropListFieldBuilder { +class IdentityInputWithDropListFieldBuilder extends StatelessWidget { final String _label; final String? _error; final TextEditingController? editingController; + final FocusNode? focusNode; + final OnSelectedSuggestionAction? onSelectedSuggestionAction; + final OnSuggestionCallbackAction? onSuggestionCallbackAction; + final OnChangeInputSuggestionAction? onChangeInputSuggestionAction; - OnSelectedSuggestionAction? _onSelectedSuggestionAction; - OnSuggestionCallbackAction? _onSuggestionCallbackAction; - OnChangeInputSuggestionAction? _onChangeInputSuggestionAction; - - IdentityInputWithDropListFieldBuilder( + const IdentityInputWithDropListFieldBuilder( this._label, this._error, - this.editingController, - ); - - void addOnSelectedSuggestionAction(OnSelectedSuggestionAction action) { - _onSelectedSuggestionAction = action; - } - - void addOnSuggestionCallbackAction(OnSuggestionCallbackAction action) { - _onSuggestionCallbackAction = action; - } - - void addOnChangeInputSuggestionAction(OnChangeInputSuggestionAction action) { - _onChangeInputSuggestionAction = action; - } + this.editingController, { + super.key, + this.focusNode, + this.onSelectedSuggestionAction, + this.onSuggestionCallbackAction, + this.onChangeInputSuggestionAction, + }); - Widget build() { + @override + Widget build(BuildContext context) { return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(_label, style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: AppColor.colorContentEmail)), + Text( + _label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: AppColor.colorContentEmail)), const SizedBox(height: 8), - TypeAheadFormField( - textFieldConfiguration: TextFieldConfiguration( - controller: editingController, - textInputAction: TextInputAction.done, - decoration: (IdentityInputDecorationBuilder() - ..setContentPadding(const EdgeInsets.symmetric( - vertical: BuildUtils.isWeb ? 16 : 12, - horizontal: 12)) - ..setErrorText(_error)) - .build() - ), + TypeAheadFormFieldBuilder( + focusNode: focusNode, + controller: editingController, + textInputAction: TextInputAction.done, + decoration: (IdentityInputDecorationBuilder() + ..setContentPadding(const EdgeInsets.symmetric( + vertical: PlatformInfo.isWeb ? 16 : 12, + horizontal: 12)) + ..setErrorText(_error)) + .build(), debounceDuration: const Duration(milliseconds: 500), suggestionsCallback: (pattern) async { - if (_onChangeInputSuggestionAction != null) { - _onChangeInputSuggestionAction!(pattern); + if (onChangeInputSuggestionAction != null) { + onChangeInputSuggestionAction!(pattern); } - if (_onSuggestionCallbackAction != null) { - return _onSuggestionCallbackAction!(pattern); + if (onSuggestionCallbackAction != null) { + return onSuggestionCallbackAction!(pattern); } else { return []; } @@ -69,16 +66,16 @@ class IdentityInputWithDropListFieldBuilder { itemBuilder: (BuildContext context, emailAddress) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Text(emailAddress.email ?? '', + child: Text( + emailAddress.email ?? '', style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: Colors.black)), - ); + fontSize: 14, + fontWeight: FontWeight.normal, + color: Colors.black))); }, onSuggestionSelected: (emailSelected) { - if (_onSelectedSuggestionAction != null) { - _onSelectedSuggestionAction!(emailSelected); + if (onSelectedSuggestionAction != null) { + onSelectedSuggestionAction!(emailSelected); } }, suggestionsBoxDecoration: SuggestionsBoxDecoration( diff --git a/lib/features/identity_creator/presentation/widgets/set_default_identity_checkbox_builder.dart b/lib/features/identity_creator/presentation/widgets/set_default_identity_checkbox_builder.dart index 4254c94a77..fa4e530c95 100644 --- a/lib/features/identity_creator/presentation/widgets/set_default_identity_checkbox_builder.dart +++ b/lib/features/identity_creator/presentation/widgets/set_default_identity_checkbox_builder.dart @@ -32,18 +32,18 @@ class SetDefaultIdentityCheckboxBuilder extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(4.0), border: Border.all(color: AppColor.primaryColor, width: 2.0), - color: isCheck ? AppColor.primaryColor : Colors.transparent, + color: isCheck ? AppColor.primaryColor : Colors.white, ), child: SvgPicture.asset( imagePaths.icSelectedSB, - color: isCheck ? Colors.white : Colors.transparent, + colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn), width: 18, height: 18, )), ), ), - Padding( - padding: const EdgeInsets.only(left: 8.0), + const SizedBox(width: 8), + Flexible( child: Text( AppLocalizations.of(context).setDefaultIdentity, style: const TextStyle( diff --git a/lib/features/login/data/datasource/account_datasource.dart b/lib/features/login/data/datasource/account_datasource.dart index 2e88d9a61f..1f145075bd 100644 --- a/lib/features/login/data/datasource/account_datasource.dart +++ b/lib/features/login/data/datasource/account_datasource.dart @@ -1,9 +1,9 @@ -import 'package:model/account/account.dart'; +import 'package:model/account/personal_account.dart'; abstract class AccountDatasource { - Future getCurrentAccount(); + Future getCurrentAccount(); - Future setCurrentAccount(Account newCurrentAccount); + Future setCurrentAccount(PersonalAccount newCurrentAccount); Future deleteCurrentAccount(String accountId); } \ No newline at end of file diff --git a/lib/features/login/data/datasource/authentication_datasource.dart b/lib/features/login/data/datasource/authentication_datasource.dart index e5ae2a6a6a..a7f775ee71 100644 --- a/lib/features/login/data/datasource/authentication_datasource.dart +++ b/lib/features/login/data/datasource/authentication_datasource.dart @@ -1,4 +1,6 @@ -import 'package:model/model.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/password.dart'; +import 'package:model/user/user_profile.dart'; abstract class AuthenticationDataSource { Future authenticationUser(Uri baseUrl, UserName userName, Password password); diff --git a/lib/features/login/data/datasource/authentication_oidc_datasource.dart b/lib/features/login/data/datasource/authentication_oidc_datasource.dart index fc198603df..d64a642430 100644 --- a/lib/features/login/data/datasource/authentication_oidc_datasource.dart +++ b/lib/features/login/data/datasource/authentication_oidc_datasource.dart @@ -6,6 +6,8 @@ abstract class AuthenticationOIDCDataSource { Future getOIDCConfiguration(OIDCResponse oidcResponse); + Future discoverOIDC(OIDCConfiguration oidcConfiguration); + Future getTokenOIDC(String clientId, String redirectUrl, String discoveryUrl, List scopes); Future persistTokenOIDC(TokenOIDC tokenOidc); @@ -27,7 +29,7 @@ abstract class AuthenticationOIDCDataSource { List scopes, String refreshToken); - Future logout(TokenId tokenId, OIDCConfiguration config); + Future logout(TokenId tokenId, OIDCConfiguration config, OIDCDiscoveryResponse oidcRescovery); Future authenticateOidcOnBrowser( String clientId, @@ -35,5 +37,5 @@ abstract class AuthenticationOIDCDataSource { String discoveryUrl, List scopes); - Future getAuthenticationInfo(); + Future getAuthenticationInfo(); } \ No newline at end of file diff --git a/lib/features/login/data/datasource_impl/authentication_datasource_impl.dart b/lib/features/login/data/datasource_impl/authentication_datasource_impl.dart index 57796f5b34..781563a414 100644 --- a/lib/features/login/data/datasource_impl/authentication_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/authentication_datasource_impl.dart @@ -1,4 +1,6 @@ -import 'package:model/model.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/password.dart'; +import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/login/data/datasource/authentication_datasource.dart'; class AuthenticationDataSourceImpl extends AuthenticationDataSource { @@ -8,7 +10,7 @@ class AuthenticationDataSourceImpl extends AuthenticationDataSource { @override Future authenticationUser(Uri baseUrl, UserName userName, Password password) { return Future.sync(() { - return UserProfile(userName.userName); + return UserProfile(userName.value); }); } } \ No newline at end of file diff --git a/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart b/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart index 93b3a68256..dbaa6e0163 100644 --- a/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart @@ -25,65 +25,57 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource { @override Future checkOIDCIsAvailable(OIDCRequest oidcRequest) { return Future.sync(() async { - final oidcResponse = await _oidcHttpClient.checkOIDCIsAvailable(oidcRequest); - return oidcResponse!; - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await _oidcHttpClient.checkOIDCIsAvailable(oidcRequest); + }).catchError(_exceptionThrower.throwException); } @override Future getOIDCConfiguration(OIDCResponse oidcResponse) { return Future.sync(() async { return await _oidcHttpClient.getOIDCConfiguration(oidcResponse); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future discoverOIDC(OIDCConfiguration oidcConfiguration) { + return Future.sync(() async { + return await _oidcHttpClient.discoverOIDC(oidcConfiguration); + }).catchError(_exceptionThrower.throwException); } @override Future getTokenOIDC(String clientId, String redirectUrl, String discoveryUrl, List scopes) { return Future.sync(() async { return await _authenticationClient.getTokenOIDC(clientId, redirectUrl, discoveryUrl, scopes); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future getStoredTokenOIDC(String tokenIdHash) { return Future.sync(() async { return await _tokenOidcCacheManager.getTokenOidc(tokenIdHash); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future persistTokenOIDC(TokenOIDC tokenOidc) { return Future.sync(() async { return await _tokenOidcCacheManager.persistOneTokenOidc(tokenOidc); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future getStoredOidcConfiguration() { return Future.sync(() async { return await _oidcConfigurationCacheManager.getOidcConfiguration(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future persistAuthorityOidc(String authority) { return Future.sync(() async { return await _oidcConfigurationCacheManager.persistAuthorityOidc(authority); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override @@ -101,27 +93,21 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource { discoveryUrl, scopes, refreshToken); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future logout(TokenId tokenId, OIDCConfiguration config) { + Future logout(TokenId tokenId, OIDCConfiguration config, OIDCDiscoveryResponse oidcRescovery) { return Future.sync(() async { - return await _authenticationClient.logoutOidc(tokenId, config); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await _authenticationClient.logoutOidc(tokenId, config, oidcRescovery); + }).catchError(_exceptionThrower.throwException); } @override Future deleteAuthorityOidc() { return Future.sync(() async { return await _oidcConfigurationCacheManager.deleteAuthorityOidc(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override @@ -137,26 +123,20 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource { redirectUrl, discoveryUrl, scopes); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future getAuthenticationInfo() { + Future getAuthenticationInfo() { return Future.sync(() async { return await _authenticationClient.getAuthenticationInfo(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future deleteTokenOIDC() { return Future.sync(() async { return await _tokenOidcCacheManager.deleteTokenOidc(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/login/data/datasource_impl/hive_account_datasource_impl.dart b/lib/features/login/data/datasource_impl/hive_account_datasource_impl.dart index e18ab99e08..e5cc4ef615 100644 --- a/lib/features/login/data/datasource_impl/hive_account_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/hive_account_datasource_impl.dart @@ -1,4 +1,4 @@ -import 'package:model/account/account.dart'; +import 'package:model/account/personal_account.dart'; import 'package:tmail_ui_user/features/login/data/datasource/account_datasource.dart'; import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; @@ -11,29 +11,23 @@ class HiveAccountDatasourceImpl extends AccountDatasource { HiveAccountDatasourceImpl(this._accountCacheManager, this._exceptionThrower); @override - Future getCurrentAccount() { + Future getCurrentAccount() { return Future.sync(() async { return await _accountCacheManager.getSelectedAccount(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future setCurrentAccount(Account newCurrentAccount) { + Future setCurrentAccount(PersonalAccount newCurrentAccount) { return Future.sync(() async { return await _accountCacheManager.setSelectedAccount(newCurrentAccount); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future deleteCurrentAccount(String accountId) { return Future.sync(() async { return await _accountCacheManager.deleteSelectedAccount(accountId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/login/data/datasource_impl/login_url_datasource_impl.dart b/lib/features/login/data/datasource_impl/login_url_datasource_impl.dart index b30bb9d20f..565ab02728 100644 --- a/lib/features/login/data/datasource_impl/login_url_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/login_url_datasource_impl.dart @@ -1,4 +1,4 @@ -import 'package:tmail_ui_user/features/caching/recent_login_url_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_login_url_cache_client.dart'; import 'package:tmail_ui_user/features/login/data/datasource/login_url_datasource.dart'; import 'package:tmail_ui_user/features/login/data/model/recent_login_url_cache.dart'; import 'package:tmail_ui_user/features/login/domain/extensions/list_recent_login_url_extension.dart'; @@ -24,9 +24,7 @@ class LoginUrlDataSourceImpl implements LoginUrlDataSource { recentLoginUrl.url, recentLoginUrl.toRecentLoginUrlCache()); } - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override @@ -46,9 +44,7 @@ class LoginUrlDataSourceImpl implements LoginUrlDataSource { : listRecentUrl; return newListRecentSUrl; - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } bool _filterRecentUrlCache(RecentLoginUrlCache recentLoginUrlCache, String? pattern) { diff --git a/lib/features/login/data/datasource_impl/login_username_datasource_impl.dart b/lib/features/login/data/datasource_impl/login_username_datasource_impl.dart index d549b3e2f9..2304865816 100644 --- a/lib/features/login/data/datasource_impl/login_username_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/login_username_datasource_impl.dart @@ -1,4 +1,4 @@ -import 'package:tmail_ui_user/features/caching/recent_login_username_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_login_username_cache_client.dart'; import 'package:tmail_ui_user/features/login/data/datasource/login_username_datasource.dart'; import 'package:tmail_ui_user/features/login/data/model/recent_login_username_cache.dart'; import 'package:tmail_ui_user/features/login/domain/extensions/list_recent_login_username_extension.dart'; @@ -28,9 +28,7 @@ class LoginUsernameDataSourceImpl implements LoginUsernameDataSource { return listValidRecentUsername.length > newLimit ? listValidRecentUsername.sublist(0, newLimit) : listValidRecentUsername; - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override @@ -44,9 +42,7 @@ class LoginUsernameDataSourceImpl implements LoginUsernameDataSource { await _recentLoginUsernameCacheClient.insertItem(recentLoginUsername.username, recentLoginUsername.toRecentLoginUsernameCache()); } - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } bool _filterRecentLoginUsernameCache( diff --git a/lib/features/login/data/extensions/account_cache_extensions.dart b/lib/features/login/data/extensions/account_cache_extensions.dart index 21e83dc5f7..20c61ed719 100644 --- a/lib/features/login/data/extensions/account_cache_extensions.dart +++ b/lib/features/login/data/extensions/account_cache_extensions.dart @@ -1,7 +1,8 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; -import 'package:model/account/account.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/authentication_type.dart'; +import 'package:model/account/personal_account.dart'; import 'package:tmail_ui_user/features/login/data/model/account_cache.dart'; extension AccountCacheExtension on AccountCache { @@ -15,14 +16,14 @@ extension AccountCacheExtension on AccountCache { } } - Account toAccount() { + PersonalAccount toAccount() { final authenticationType = fromAuthenticationTypeString(); - return Account( + return PersonalAccount( id, authenticationType, isSelected: isSelected, accountId: accountId != null ? AccountId(Id(accountId!)) : null, - apiUrl: apiUrl - ); + apiUrl: apiUrl, + userName: userName != null ? UserName(userName!) : null); } } \ No newline at end of file diff --git a/lib/features/login/data/extensions/account_extensions.dart b/lib/features/login/data/extensions/personal_account_extension.dart similarity index 50% rename from lib/features/login/data/extensions/account_extensions.dart rename to lib/features/login/data/extensions/personal_account_extension.dart index 8486f2b2bf..c36dcb93cc 100644 --- a/lib/features/login/data/extensions/account_extensions.dart +++ b/lib/features/login/data/extensions/personal_account_extension.dart @@ -1,15 +1,14 @@ -import 'package:model/account/account.dart'; -import 'package:model/account/authentication_type.dart'; +import 'package:model/account/personal_account.dart'; import 'package:tmail_ui_user/features/login/data/model/account_cache.dart'; -extension AccountExtensions on Account { +extension PersonalAccountExtension on PersonalAccount { AccountCache toCache() { return AccountCache( id, - authenticationType.asString(), + authenticationType.name, isSelected: isSelected, accountId: accountId?.id.value, - apiUrl: apiUrl - ); + apiUrl: apiUrl, + userName: userName?.value); } } \ No newline at end of file diff --git a/lib/features/login/data/extensions/token_response_extension.dart b/lib/features/login/data/extensions/token_response_extension.dart index 3807bdc7a1..fbfff79109 100644 --- a/lib/features/login/data/extensions/token_response_extension.dart +++ b/lib/features/login/data/extensions/token_response_extension.dart @@ -4,11 +4,11 @@ import 'package:model/model.dart'; extension TokenResponseExtension on TokenResponse { - TokenOIDC toTokenOIDC() { + TokenOIDC toTokenOIDC({String? maybeAvailableRefreshToken}) { return TokenOIDC( accessToken ?? '', TokenId(idToken ?? ''), - refreshToken ?? '', + refreshToken ?? maybeAvailableRefreshToken ?? '', expiredTime: accessTokenExpirationDateTime ?? DateTime.now()); } } \ No newline at end of file diff --git a/lib/features/login/data/local/account_cache_manager.dart b/lib/features/login/data/local/account_cache_manager.dart index 86494822c3..a688309808 100644 --- a/lib/features/login/data/local/account_cache_manager.dart +++ b/lib/features/login/data/local/account_cache_manager.dart @@ -1,8 +1,8 @@ import 'package:core/utils/app_logger.dart'; -import 'package:model/account/account.dart'; -import 'package:tmail_ui_user/features/caching/account_cache_client.dart'; +import 'package:model/account/personal_account.dart'; +import 'package:tmail_ui_user/features/caching/clients/account_cache_client.dart'; import 'package:tmail_ui_user/features/login/data/extensions/account_cache_extensions.dart'; -import 'package:tmail_ui_user/features/login/data/extensions/account_extensions.dart'; +import 'package:tmail_ui_user/features/login/data/extensions/personal_account_extension.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; class AccountCacheManager { @@ -10,7 +10,7 @@ class AccountCacheManager { AccountCacheManager(this._accountCacheClient); - Future getSelectedAccount() async { + Future getSelectedAccount() async { try { final allAccounts = await _accountCacheClient.getAll(); return allAccounts.firstWhere((account) => account.isSelected) @@ -21,7 +21,7 @@ class AccountCacheManager { } } - Future setSelectedAccount(Account account) { + Future setSelectedAccount(PersonalAccount account) { log('AccountCacheManager::setSelectedAccount(): $_accountCacheClient'); return _accountCacheClient.insertItem(account.id, account.toCache()); } diff --git a/lib/features/login/data/local/authentication_info_cache_manager.dart b/lib/features/login/data/local/authentication_info_cache_manager.dart index fb3c8495ce..e200a361ab 100644 --- a/lib/features/login/data/local/authentication_info_cache_manager.dart +++ b/lib/features/login/data/local/authentication_info_cache_manager.dart @@ -1,4 +1,4 @@ -import 'package:tmail_ui_user/features/caching/authentication_info_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/authentication_info_cache_client.dart'; import 'package:tmail_ui_user/features/login/data/model/authentication_info_cache.dart'; class AuthenticationInfoCacheManager { diff --git a/lib/features/login/data/local/encryption_key_cache_manager.dart b/lib/features/login/data/local/encryption_key_cache_manager.dart index 2a89b6b191..bb3720fc76 100644 --- a/lib/features/login/data/local/encryption_key_cache_manager.dart +++ b/lib/features/login/data/local/encryption_key_cache_manager.dart @@ -1,4 +1,4 @@ -import 'package:tmail_ui_user/features/caching/encryption_key_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/encryption_key_cache_client.dart'; import 'package:tmail_ui_user/features/login/data/model/encryption_key_cache.dart'; class EncryptionKeyCacheManager { diff --git a/lib/features/login/data/local/oidc_configuration_cache_manager.dart b/lib/features/login/data/local/oidc_configuration_cache_manager.dart index a8590d42be..88ed6988f8 100644 --- a/lib/features/login/data/local/oidc_configuration_cache_manager.dart +++ b/lib/features/login/data/local/oidc_configuration_cache_manager.dart @@ -3,6 +3,7 @@ import 'package:model/oidc/oidc_configuration.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; class OidcConfigurationCacheManager { final SharedPreferences _sharedPreferences; @@ -17,7 +18,7 @@ class OidcConfigurationCacheManager { return OIDCConfiguration( authority: authority, clientId: OIDCConstant.clientId, - scopes: OIDCConstant.oidcScope); + scopes: AppConfig.oidcScopes); } } diff --git a/lib/features/login/data/local/token_oidc_cache_manager.dart b/lib/features/login/data/local/token_oidc_cache_manager.dart index ebedb9c57e..1dd6d2c8ba 100644 --- a/lib/features/login/data/local/token_oidc_cache_manager.dart +++ b/lib/features/login/data/local/token_oidc_cache_manager.dart @@ -1,6 +1,6 @@ import 'package:core/utils/app_logger.dart'; import 'package:model/oidc/token_oidc.dart'; -import 'package:tmail_ui_user/features/caching/token_oidc_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/token_oidc_cache_client.dart'; import 'package:tmail_ui_user/features/login/data/extensions/token_oidc_cache_extension.dart'; import 'package:tmail_ui_user/features/login/data/extensions/token_oidc_extension.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; diff --git a/lib/features/login/data/model/account_cache.dart b/lib/features/login/data/model/account_cache.dart index c211790cb5..5553d78b56 100644 --- a/lib/features/login/data/model/account_cache.dart +++ b/lib/features/login/data/model/account_cache.dart @@ -21,13 +21,17 @@ class AccountCache extends HiveObject with EquatableMixin { @HiveField(4) final String? apiUrl; + @HiveField(5) + final String? userName; + AccountCache( this.id, this.authenticationType, { required this.isSelected, this.accountId, - this.apiUrl + this.apiUrl, + this.userName } ); @@ -37,6 +41,7 @@ class AccountCache extends HiveObject with EquatableMixin { authenticationType, isSelected, accountId, - apiUrl + apiUrl, + userName ]; } \ No newline at end of file diff --git a/lib/features/login/data/network/authentication_client/authentication_client_base.dart b/lib/features/login/data/network/authentication_client/authentication_client_base.dart index 97ab33043b..4ecc390f9a 100644 --- a/lib/features/login/data/network/authentication_client/authentication_client_base.dart +++ b/lib/features/login/data/network/authentication_client/authentication_client_base.dart @@ -1,5 +1,6 @@ import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/response/oidc_discovery_response.dart'; import 'package:model/oidc/token_id.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_mobile.dart' @@ -12,7 +13,7 @@ abstract class AuthenticationClientBase { String discoveryUrl, List scopes); - Future getAuthenticationInfo(); + Future getAuthenticationInfo(); Future getTokenOIDC( String clientId, @@ -27,7 +28,7 @@ abstract class AuthenticationClientBase { List scopes, String refreshToken); - Future logoutOidc(TokenId tokenId, OIDCConfiguration config); + Future logoutOidc(TokenId tokenId, OIDCConfiguration config, OIDCDiscoveryResponse oidcRescovery); factory AuthenticationClientBase() => getAuthenticationClientImplementation(); } \ No newline at end of file diff --git a/lib/features/login/data/network/authentication_client/authentication_client_mobile.dart b/lib/features/login/data/network/authentication_client/authentication_client_mobile.dart index 1947d1ff62..9bfe112830 100644 --- a/lib/features/login/data/network/authentication_client/authentication_client_mobile.dart +++ b/lib/features/login/data/network/authentication_client/authentication_client_mobile.dart @@ -3,13 +3,14 @@ import 'package:core/utils/app_logger.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:get/get.dart'; import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/response/oidc_discovery_response.dart'; import 'package:model/oidc/token_id.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/login/data/extensions/authentication_token_extension.dart'; -import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; import 'package:tmail_ui_user/features/login/data/extensions/token_response_extension.dart'; import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; +import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; class AuthenticationClientMobile implements AuthenticationClientBase { @@ -43,11 +44,19 @@ class AuthenticationClientMobile implements AuthenticationClientBase { } @override - Future logoutOidc(TokenId tokenId, OIDCConfiguration config) async { + Future logoutOidc(TokenId tokenId, OIDCConfiguration config, OIDCDiscoveryResponse oidcRescovery) async { + final authorizationServiceConfiguration = oidcRescovery.authorizationEndpoint == null || oidcRescovery.tokenEndpoint == null + ? null + : AuthorizationServiceConfiguration( + authorizationEndpoint: oidcRescovery.authorizationEndpoint!, + tokenEndpoint: oidcRescovery.tokenEndpoint!, + endSessionEndpoint: oidcRescovery.endSessionEndpoint); + final endSession = await _appAuth.endSession(EndSessionRequest( idTokenHint: tokenId.uuid, postLogoutRedirectUrl: config.logoutRedirectUrl, - discoveryUrl: config.discoveryUrl + discoveryUrl: config.discoveryUrl, + serviceConfiguration: authorizationServiceConfiguration )); log('AuthenticationClientMobile::logoutOidc(): ${endSession?.state}'); return endSession?.state?.isNotEmpty == true; @@ -66,7 +75,7 @@ class AuthenticationClientMobile implements AuthenticationClientBase { log('AuthenticationClientMobile::refreshingTokensOIDC(): refreshToken: ${tokenResponse?.accessToken}'); if (tokenResponse != null) { - final tokenOIDC = tokenResponse.toTokenOIDC(); + final tokenOIDC = tokenResponse.toTokenOIDC(maybeAvailableRefreshToken: refreshToken); if (tokenOIDC.isTokenValid()) { return tokenOIDC; } else { @@ -84,8 +93,8 @@ class AuthenticationClientMobile implements AuthenticationClientBase { } @override - Future getAuthenticationInfo() { - return Future.value(null); + Future getAuthenticationInfo() { + return Future.value(''); } } diff --git a/lib/features/login/data/network/authentication_client/authentication_client_web.dart b/lib/features/login/data/network/authentication_client/authentication_client_web.dart index 4c2f72b4f0..1544923c0b 100644 --- a/lib/features/login/data/network/authentication_client/authentication_client_web.dart +++ b/lib/features/login/data/network/authentication_client/authentication_client_web.dart @@ -1,18 +1,19 @@ import 'package:core/utils/app_logger.dart'; +import 'package:flutter_appauth_platform_interface/flutter_appauth_platform_interface.dart'; import 'package:get/get.dart'; +import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/response/oidc_discovery_response.dart'; +import 'package:model/oidc/token_id.dart'; +import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/login/data/extensions/authentication_token_extension.dart'; -import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; import 'package:tmail_ui_user/features/login/data/extensions/token_response_extension.dart'; +import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; import 'package:tmail_ui_user/features/login/data/utils/library_platform/app_auth_plugin/app_auth_plugin.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; +import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; import 'package:universal_html/html.dart' as html; -import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; -import 'package:flutter_appauth_platform_interface/flutter_appauth_platform_interface.dart'; -import 'package:model/oidc/oidc_configuration.dart'; -import 'package:model/oidc/token_id.dart'; -import 'package:model/oidc/token_oidc.dart'; class AuthenticationClientWeb implements AuthenticationClientBase { @@ -45,11 +46,18 @@ class AuthenticationClientWeb implements AuthenticationClientBase { } @override - Future logoutOidc(TokenId tokenId, OIDCConfiguration config) async { + Future logoutOidc(TokenId tokenId, OIDCConfiguration config, OIDCDiscoveryResponse oidcRescovery) async { + final authorizationServiceConfiguration = oidcRescovery.authorizationEndpoint == null || oidcRescovery.tokenEndpoint == null + ? null + : AuthorizationServiceConfiguration( + authorizationEndpoint: oidcRescovery.authorizationEndpoint!, + tokenEndpoint: oidcRescovery.tokenEndpoint!, + endSessionEndpoint: oidcRescovery.endSessionEndpoint); final endSession = await _appAuthWeb.endSession(EndSessionRequest( idTokenHint: tokenId.uuid, postLogoutRedirectUrl: config.logoutRedirectUrl, - discoveryUrl: config.discoveryUrl + discoveryUrl: config.discoveryUrl, + serviceConfiguration: authorizationServiceConfiguration )); return endSession != null; } @@ -66,7 +74,7 @@ class AuthenticationClientWeb implements AuthenticationClientBase { scopes: scopes)); if (tokenResponse != null) { - final tokenOIDC = tokenResponse.toTokenOIDC(); + final tokenOIDC = tokenResponse.toTokenOIDC(maybeAvailableRefreshToken: refreshToken); if (tokenOIDC.isTokenValid()) { return tokenOIDC; } else { @@ -89,10 +97,14 @@ class AuthenticationClientWeb implements AuthenticationClientBase { } @override - Future getAuthenticationInfo() async { + Future getAuthenticationInfo() async { final authUrl = html.window.sessionStorage[OIDCConstant.authResponseKey]; log('AuthenticationClientWeb::getAuthenticationInfo(): authUrl: $authUrl'); - return authUrl; + if (authUrl != null && authUrl.isNotEmpty) { + return authUrl; + } else { + throw CanNotAuthenticationInfoOnWeb(); + } } } diff --git a/lib/features/login/data/network/config/authorization_interceptors.dart b/lib/features/login/data/network/config/authorization_interceptors.dart index a122e2bf20..9a7bdfac67 100644 --- a/lib/features/login/data/network/config/authorization_interceptors.dart +++ b/lib/features/login/data/network/config/authorization_interceptors.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:core/utils/app_logger.dart'; import 'package:dio/dio.dart'; -import 'package:model/account/account.dart'; +import 'package:model/account/personal_account.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/token.dart'; @@ -13,7 +13,10 @@ import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.da import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; -class AuthorizationInterceptors extends InterceptorsWrapper { +class AuthorizationInterceptors extends QueuedInterceptorsWrapper { + + static const int _maxRetryCount = 3; + static const String RETRY_KEY = 'Retry'; final Dio _dio; final AuthenticationClientBase _authenticationClient; @@ -41,9 +44,6 @@ class AuthorizationInterceptors extends InterceptorsWrapper { _token = newToken; _configOIDC = newConfig; _authenticationType = AuthenticationType.oidc; - log('AuthorizationInterceptors::setToken(): newToken: $newToken'); - log('AuthorizationInterceptors::setToken(): tokenId: ${newToken?.tokenIdHash}'); - log('AuthorizationInterceptors::setToken(): EXPIRE_DATE: ${newToken?.expiredTime?.toIso8601String()}'); } void _updateNewToken(Token newToken) { @@ -56,6 +56,8 @@ class AuthorizationInterceptors extends InterceptorsWrapper { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + log('AuthorizationInterceptors::onRequest():DATA: ${options.data}'); + log('AuthorizationInterceptors::onRequest():TOKEN_HASHCODE_CURRENT: ${_token?.token.hashCode}'); switch(_authenticationType) { case AuthenticationType.basic: if (_authorization != null) { @@ -75,65 +77,87 @@ class AuthorizationInterceptors extends InterceptorsWrapper { @override void onError(DioError err, ErrorInterceptorHandler handler) async { + logError('AuthorizationInterceptors::onError(): $err'); + logError('AuthorizationInterceptors::onError():TOKEN_HASHCODE_CURRENT: ${_token?.token.hashCode}'); + final requestOptions = err.requestOptions; - log('AuthorizationInterceptors::onError(): $err'); - if (_isTokenExpired() && - err.response?.statusCode == 401 && - _isAuthenticationOidcValid()) { - try { - final newToken = await _authenticationClient.refreshingTokensOIDC( - _configOIDC!.clientId, - _configOIDC!.redirectUrl, - _configOIDC!.discoveryUrl, - _configOIDC!.scopes, - _token!.refreshToken); - - final accountCurrent = await _accountCacheManager.getSelectedAccount(); - - await Future.wait([ - _tokenOidcCacheManager.persistOneTokenOidc(newToken), - _accountCacheManager.deleteSelectedAccount(_token!.tokenIdHash), - _accountCacheManager.setSelectedAccount(Account( + final extraInRequest = requestOptions.extra; + var retries = extraInRequest[RETRY_KEY] ?? 0; + + if (_validateToRefreshToken(err)) { + log('AuthorizationInterceptors::onError:>> _validateToRefreshToken'); + final newToken = await _authenticationClient.refreshingTokensOIDC( + _configOIDC!.clientId, + _configOIDC!.redirectUrl, + _configOIDC!.discoveryUrl, + _configOIDC!.scopes, + _token!.refreshToken + ); + + final accountCurrent = await _accountCacheManager.getSelectedAccount(); + + await _accountCacheManager.deleteSelectedAccount(_token!.tokenIdHash); + + await Future.wait([ + _tokenOidcCacheManager.persistOneTokenOidc(newToken), + _accountCacheManager.setSelectedAccount( + PersonalAccount( newToken.tokenIdHash, AuthenticationType.oidc, isSelected: true, accountId: accountCurrent.accountId, - apiUrl: accountCurrent.apiUrl - )), - ]); - - log('AuthorizationInterceptors::onError(): refreshToken: $newToken'); - log('AuthorizationInterceptors::setToken(): refreshTokenId: ${newToken.tokenIdHash}'); - - _updateNewToken(newToken.toToken()); - - requestOptions.headers[HttpHeaders.authorizationHeader] = - _getTokenAsBearerHeader(newToken.token); - - final response = await _dio.fetch(requestOptions); - return handler.resolve(response); - } catch(e) { - log('AuthorizationInterceptors::onError(): $e'); - super.onError(err, handler); - } + apiUrl: accountCurrent.apiUrl, + userName: accountCurrent.userName + ) + ) + ]); + _updateNewToken(newToken.toToken()); + + final requestOptions = err.requestOptions; + requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(newToken.token); + + final response = await _dio.fetch(requestOptions); + return handler.resolve(response); + } else if (_validateToRetry(err, retries)) { + log('AuthorizationInterceptors::onError:>> _validateToRetry | retries: $retries'); + retries++; + + final requestOptions = err.requestOptions; + requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(_token!.token); + requestOptions.extra = {RETRY_KEY: retries}; + + final response = await _dio.fetch(requestOptions); + return handler.resolve(response); } else { super.onError(err, handler); } } - bool _isTokenExpired() { - if (_token?.isExpired == true) { - log('AuthorizationInterceptors::_isTokenExpired(): TOKE_EXPIRED'); + bool _isTokenExpired() => _token?.isExpired == true; + + bool _isAuthenticationOidcValid() => _authenticationType == AuthenticationType.oidc && _configOIDC != null; + + bool _isTokenNotEmpty() => _token?.token.isNotEmpty == true; + + bool _isRefreshTokenNotEmpty() => _token?.refreshToken.isNotEmpty == true; + + bool _validateToRefreshToken(DioError dioError) { + if (dioError.response?.statusCode == 401 && + _isAuthenticationOidcValid() && + _isRefreshTokenNotEmpty() && + _isTokenExpired() + ) { return true; } return false; } - bool _isAuthenticationOidcValid() { - if (_authenticationType == AuthenticationType.oidc && - _configOIDC != null && - _token != null) { - log('AuthorizationInterceptors::_isAuthenticationOidcValid()'); + bool _validateToRetry(DioError dioError, int retryCount) { + if (dioError.type == DioErrorType.badResponse && + dioError.response?.statusCode == 401 && + _isTokenNotEmpty() && + retryCount < _maxRetryCount + ) { return true; } return false; @@ -143,6 +167,17 @@ class AuthorizationInterceptors extends InterceptorsWrapper { String _getTokenAsBearerHeader(String token) => 'Bearer $token'; + bool get isAppRunning { + switch(_authenticationType) { + case AuthenticationType.basic: + return _authorization != null; + case AuthenticationType.oidc: + return _configOIDC != null && _token != null; + case AuthenticationType.none: + return false; + } + } + void clear() { _authorization = null; _token = null; diff --git a/lib/features/login/data/network/config/oidc_constant.dart b/lib/features/login/data/network/config/oidc_constant.dart index d548462309..8940791367 100644 --- a/lib/features/login/data/network/config/oidc_constant.dart +++ b/lib/features/login/data/network/config/oidc_constant.dart @@ -3,9 +3,9 @@ import 'package:tmail_ui_user/main/utils/app_config.dart'; class OIDCConstant { static String get mobileOidcClientId => 'teammail-mobile'; - static List get oidcScope => ['openid', 'offline_access']; + static List get oidcScope => ['openid', 'profile', 'email', 'offline_access']; static const keyAuthorityOidc = 'KEY_AUTHORITY_OIDC'; static const authResponseKey = "auth_info"; - static String get clientId => BuildUtils.isWeb ? AppConfig.webOidcClientId : mobileOidcClientId; + static String get clientId => PlatformInfo.isWeb ? AppConfig.webOidcClientId : mobileOidcClientId; } \ No newline at end of file diff --git a/lib/features/login/data/network/oidc_error.dart b/lib/features/login/data/network/oidc_error.dart index 7e44640817..b18abf6c83 100644 --- a/lib/features/login/data/network/oidc_error.dart +++ b/lib/features/login/data/network/oidc_error.dart @@ -1 +1,5 @@ -class CanNotFoundOIDCAuthority implements Exception {} \ No newline at end of file +class CanNotFoundOIDCAuthority implements Exception {} + +class CanNotFoundOIDCLinks implements Exception {} + +class CanNotFoundToken implements Exception {} \ No newline at end of file diff --git a/lib/features/login/data/network/oidc_http_client.dart b/lib/features/login/data/network/oidc_http_client.dart index b6f503fcbb..808940c682 100644 --- a/lib/features/login/data/network/oidc_http_client.dart +++ b/lib/features/login/data/network/oidc_http_client.dart @@ -6,11 +6,13 @@ import 'package:core/data/network/dio_client.dart'; import 'package:core/utils/app_logger.dart'; import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/request/oidc_request.dart'; +import 'package:model/oidc/response/oidc_discovery_response.dart'; import 'package:model/oidc/response/oidc_response.dart'; import 'package:tmail_ui_user/features/login/data/extensions/service_path_extension.dart'; import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; import 'package:tmail_ui_user/features/login/data/network/endpoint.dart'; import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; class OIDCHttpClient { @@ -18,7 +20,7 @@ class OIDCHttpClient { OIDCHttpClient(this._dioClient); - Future checkOIDCIsAvailable(OIDCRequest oidcRequest) async { + Future checkOIDCIsAvailable(OIDCRequest oidcRequest) async { final result = await _dioClient.get( Endpoint.webFinger .generateOIDCPath(Uri.parse(oidcRequest.baseUrl)) @@ -29,10 +31,14 @@ class OIDCHttpClient { .generateEndpointPath() ); log('OIDCHttpClient::checkOIDCIsAvailable(): RESULT: $result'); - if (result is Map) { - return OIDCResponse.fromJson(result); + if (result != null) { + if (result is Map) { + return OIDCResponse.fromJson(result); + } else { + return OIDCResponse.fromJson(jsonDecode(result)); + } } else { - return OIDCResponse.fromJson(jsonDecode(result)); + throw CanNotFoundOIDCLinks(); } } @@ -44,7 +50,17 @@ class OIDCHttpClient { return OIDCConfiguration( authority: oidcResponse.links[0].href.toString(), clientId: OIDCConstant.clientId, - scopes: OIDCConstant.oidcScope + scopes: AppConfig.oidcScopes ); } + + Future discoverOIDC(OIDCConfiguration configuration) async { + final result = await _dioClient.get(configuration.discoveryUrl); + log('OIDCHttpClient::discoverOIDC(): RESULT: $result'); + if (result is Map) { + return OIDCDiscoveryResponse.fromJson(result); + } else { + return OIDCDiscoveryResponse.fromJson(jsonDecode(result)); + } + } } \ No newline at end of file diff --git a/lib/features/login/data/repository/account_repository_impl.dart b/lib/features/login/data/repository/account_repository_impl.dart index aa4a6e077c..062d349945 100644 --- a/lib/features/login/data/repository/account_repository_impl.dart +++ b/lib/features/login/data/repository/account_repository_impl.dart @@ -1,5 +1,5 @@ import 'package:core/utils/app_logger.dart'; -import 'package:model/account/account.dart'; +import 'package:model/account/personal_account.dart'; import 'package:tmail_ui_user/features/login/data/datasource/account_datasource.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; @@ -10,12 +10,12 @@ class AccountRepositoryImpl extends AccountRepository { AccountRepositoryImpl(this._accountDatasource); @override - Future getCurrentAccount() { + Future getCurrentAccount() { return _accountDatasource.getCurrentAccount(); } @override - Future setCurrentAccount(Account newCurrentAccount) { + Future setCurrentAccount(PersonalAccount newCurrentAccount) { log('AccountRepositoryImpl::setCurrentAccount(): $newCurrentAccount'); return _accountDatasource.setCurrentAccount(newCurrentAccount); } diff --git a/lib/features/login/data/repository/authentication_oidc_repository_impl.dart b/lib/features/login/data/repository/authentication_oidc_repository_impl.dart index d27da621ad..1d79b8cfbf 100644 --- a/lib/features/login/data/repository/authentication_oidc_repository_impl.dart +++ b/lib/features/login/data/repository/authentication_oidc_repository_impl.dart @@ -1,5 +1,6 @@ import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/request/oidc_request.dart'; +import 'package:model/oidc/response/oidc_discovery_response.dart'; import 'package:model/oidc/response/oidc_response.dart'; import 'package:model/oidc/token_id.dart'; import 'package:model/oidc/token_oidc.dart'; @@ -21,6 +22,12 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository { return _oidcDataSource.getOIDCConfiguration(oidcResponse); } + + @override + Future discoverOIDC(OIDCConfiguration oidcConfiguration) { + return _oidcDataSource.discoverOIDC(oidcConfiguration); + } + @override Future getTokenOIDC(String clientId, String redirectUrl, String discoveryUrl, List scopes) { return _oidcDataSource.getTokenOIDC(clientId, redirectUrl, discoveryUrl, scopes); @@ -63,8 +70,8 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository { } @override - Future logout(TokenId tokenId, OIDCConfiguration config) { - return _oidcDataSource.logout(tokenId, config); + Future logout(TokenId tokenId, OIDCConfiguration config, OIDCDiscoveryResponse oidcRescovery) { + return _oidcDataSource.logout(tokenId, config, oidcRescovery); } @override @@ -79,7 +86,7 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository { } @override - Future getAuthenticationInfo() { + Future getAuthenticationInfo() { return _oidcDataSource.getAuthenticationInfo(); } diff --git a/lib/features/login/data/repository/authentication_repository_impl.dart b/lib/features/login/data/repository/authentication_repository_impl.dart index 69350c9749..ea2e77e4c4 100644 --- a/lib/features/login/data/repository/authentication_repository_impl.dart +++ b/lib/features/login/data/repository/authentication_repository_impl.dart @@ -1,4 +1,6 @@ -import 'package:model/model.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/password.dart'; +import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/login/data/datasource/authentication_datasource.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_repository.dart'; diff --git a/lib/features/login/data/repository/credential_repository_impl.dart b/lib/features/login/data/repository/credential_repository_impl.dart index 5077e7bf89..13973ee97f 100644 --- a/lib/features/login/data/repository/credential_repository_impl.dart +++ b/lib/features/login/data/repository/credential_repository_impl.dart @@ -2,7 +2,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:tmail_ui_user/features/login/data/local/authentication_info_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/model/authentication_info_cache.dart'; -import 'package:tmail_ui_user/features/login/data/utils/login_constant.dart'; +import 'package:tmail_ui_user/features/login/domain/model/login_constants.dart'; import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; class CredentialRepositoryImpl extends CredentialRepository { @@ -17,17 +17,17 @@ class CredentialRepositoryImpl extends CredentialRepository { @override Future getBaseUrl() async { - return Uri.parse(sharedPreferences.getString(LoginConstant.keyBaseUrl) ?? ''); + return Uri.parse(sharedPreferences.getString(LoginConstants.KEY_BASE_URL) ?? ''); } @override Future saveBaseUrl(Uri baseUrl) async { - await sharedPreferences.setString(LoginConstant.keyBaseUrl, baseUrl.toString()); + await sharedPreferences.setString(LoginConstants.KEY_BASE_URL, baseUrl.toString()); } @override Future removeBaseUrl() async { - await sharedPreferences.remove(LoginConstant.keyBaseUrl); + await sharedPreferences.remove(LoginConstants.KEY_BASE_URL); } @override diff --git a/lib/features/login/data/utils/login_constant.dart b/lib/features/login/data/utils/login_constant.dart deleted file mode 100644 index 1a836e8e35..0000000000 --- a/lib/features/login/data/utils/login_constant.dart +++ /dev/null @@ -1,4 +0,0 @@ - -class LoginConstant { - static const String keyBaseUrl = 'KEY_BASE_URL'; -} \ No newline at end of file diff --git a/lib/features/login/domain/exceptions/authentication_exception.dart b/lib/features/login/domain/exceptions/authentication_exception.dart index 87ff5b2c76..35604d7aaa 100644 --- a/lib/features/login/domain/exceptions/authentication_exception.dart +++ b/lib/features/login/domain/exceptions/authentication_exception.dart @@ -23,13 +23,9 @@ class BadGateway extends AuthenticationException { List get props => [message]; } -class NotFoundAuthenticatedAccountException implements Exception { - NotFoundAuthenticatedAccountException(); -} +class NotFoundAuthenticatedAccountException implements Exception {} -class NotFoundStoredTokenException implements Exception { - NotFoundStoredTokenException(); -} +class NotFoundStoredTokenException implements Exception {} class InvalidBaseUrl extends AuthenticationException { InvalidBaseUrl() : super(AuthenticationException.invalidBaseUrl); @@ -38,17 +34,21 @@ class InvalidBaseUrl extends AuthenticationException { List get props => [message]; } -class NotFoundAccessTokenException implements Exception { - NotFoundAccessTokenException(); -} +class NotFoundAccessTokenException implements Exception {} -class AccessTokenInvalidException implements Exception { - AccessTokenInvalidException(); -} +class AccessTokenInvalidException implements Exception {} class DownloadAttachmentHasTokenExpiredException implements Exception { final String refreshToken; DownloadAttachmentHasTokenExpiredException(this.refreshToken); -} \ No newline at end of file +} + +class CanNotFoundBaseUrl implements Exception {} + +class CanNotFoundUserName implements Exception {} + +class CanNotFoundPassword implements Exception {} + +class CanNotAuthenticationInfoOnWeb implements Exception {} \ No newline at end of file diff --git a/lib/features/login/domain/extensions/oidc_configuration_extensions.dart b/lib/features/login/domain/extensions/oidc_configuration_extensions.dart index c262e799ca..fef03e30f7 100644 --- a/lib/features/login/domain/extensions/oidc_configuration_extensions.dart +++ b/lib/features/login/domain/extensions/oidc_configuration_extensions.dart @@ -1,16 +1,16 @@ -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:model/oidc/oidc_configuration.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; extension OidcConfigurationExtensions on OIDCConfiguration { String get redirectUrl { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (AppConfig.domainRedirectUrl.endsWith('/')) { return AppConfig.domainRedirectUrl + loginRedirectOidcWeb; } else { - return AppConfig.domainRedirectUrl + '/' + loginRedirectOidcWeb; + return '${AppConfig.domainRedirectUrl}/$loginRedirectOidcWeb'; } } else { return redirectOidcMobile; @@ -18,11 +18,11 @@ extension OidcConfigurationExtensions on OIDCConfiguration { } String get logoutRedirectUrl { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (AppConfig.domainRedirectUrl.endsWith('/')) { return AppConfig.domainRedirectUrl + logoutRedirectOidcWeb; } else { - return AppConfig.domainRedirectUrl + '/' + logoutRedirectOidcWeb; + return '${AppConfig.domainRedirectUrl}/$logoutRedirectOidcWeb'; } } else { return redirectOidcMobile; diff --git a/lib/features/login/domain/model/login_constants.dart b/lib/features/login/domain/model/login_constants.dart new file mode 100644 index 0000000000..ee177b3196 --- /dev/null +++ b/lib/features/login/domain/model/login_constants.dart @@ -0,0 +1,5 @@ + +class LoginConstants { + static const String KEY_BASE_URL = 'KEY_BASE_URL'; + static const String AUTH_DESTINATION_KEY = "auth_destination_url"; +} \ No newline at end of file diff --git a/lib/features/login/domain/repository/account_repository.dart b/lib/features/login/domain/repository/account_repository.dart index 3ef4aca475..f164c18b4c 100644 --- a/lib/features/login/domain/repository/account_repository.dart +++ b/lib/features/login/domain/repository/account_repository.dart @@ -1,10 +1,10 @@ -import 'package:model/account/account.dart'; +import 'package:model/account/personal_account.dart'; abstract class AccountRepository { - Future getCurrentAccount(); + Future getCurrentAccount(); - Future setCurrentAccount(Account newCurrentAccount); + Future setCurrentAccount(PersonalAccount newCurrentAccount); Future deleteCurrentAccount(String accountId); } \ No newline at end of file diff --git a/lib/features/login/domain/repository/authentication_oidc_repository.dart b/lib/features/login/domain/repository/authentication_oidc_repository.dart index 26eadd5fe1..11fa3fee3d 100644 --- a/lib/features/login/domain/repository/authentication_oidc_repository.dart +++ b/lib/features/login/domain/repository/authentication_oidc_repository.dart @@ -6,6 +6,8 @@ abstract class AuthenticationOIDCRepository { Future getOIDCConfiguration(OIDCResponse oidcResponse); + Future discoverOIDC(OIDCConfiguration oidcConfiguration); + Future getTokenOIDC(String clientId, String redirectUrl, String discoveryUrl, List scopes); Future persistTokenOIDC(TokenOIDC tokenOidc); @@ -27,7 +29,7 @@ abstract class AuthenticationOIDCRepository { List scopes, String refreshToken); - Future logout(TokenId tokenId, OIDCConfiguration config); + Future logout(TokenId tokenId, OIDCConfiguration config, OIDCDiscoveryResponse oidcRescovery); Future authenticateOidcOnBrowser( String clientId, @@ -35,5 +37,5 @@ abstract class AuthenticationOIDCRepository { String discoveryUrl, List scopes); - Future getAuthenticationInfo(); + Future getAuthenticationInfo(); } \ No newline at end of file diff --git a/lib/features/login/domain/repository/authentication_repository.dart b/lib/features/login/domain/repository/authentication_repository.dart index 01b166ac9e..03aa122dbe 100644 --- a/lib/features/login/domain/repository/authentication_repository.dart +++ b/lib/features/login/domain/repository/authentication_repository.dart @@ -1,4 +1,7 @@ -import 'package:model/model.dart'; + +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/password.dart'; +import 'package:model/user/user_profile.dart'; abstract class AuthenticationRepository { Future authenticationUser(Uri baseUrl, UserName userName, Password password); diff --git a/lib/features/login/domain/state/authenticate_oidc_on_browser_state.dart b/lib/features/login/domain/state/authenticate_oidc_on_browser_state.dart index ec6cbeae41..d3fdd58c25 100644 --- a/lib/features/login/domain/state/authenticate_oidc_on_browser_state.dart +++ b/lib/features/login/domain/state/authenticate_oidc_on_browser_state.dart @@ -1,28 +1,11 @@ - import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -class AuthenticateOidcOnBrowserLoading extends LoadingState { - - AuthenticateOidcOnBrowserLoading(); - - @override - List get props => []; -} +class AuthenticateOidcOnBrowserLoading extends LoadingState {} -class AuthenticateOidcOnBrowserSuccess extends UIState { - - AuthenticateOidcOnBrowserSuccess(); - - @override - List get props => []; -} +class AuthenticateOidcOnBrowserSuccess extends UIState {} class AuthenticateOidcOnBrowserFailure extends FeatureFailure { - final dynamic exception; - - AuthenticateOidcOnBrowserFailure(this.exception); - @override - List get props => [exception]; + AuthenticateOidcOnBrowserFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/authentication_user_state.dart b/lib/features/login/domain/state/authentication_user_state.dart index bd05648760..bf9fc83653 100644 --- a/lib/features/login/domain/state/authentication_user_state.dart +++ b/lib/features/login/domain/state/authentication_user_state.dart @@ -1,12 +1,8 @@ -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/user/user_profile.dart'; -class AuthenticationUserLoading extends LoadingState { - AuthenticationUserLoading(); - - @override - List get props => []; -} +class AuthenticationUserLoading extends LoadingState {} class AuthenticationUserSuccess extends UIState { final UserProfile userProfile; @@ -18,10 +14,6 @@ class AuthenticationUserSuccess extends UIState { } class AuthenticationUserFailure extends FeatureFailure { - final dynamic exception; - AuthenticationUserFailure(this.exception); - - @override - List get props => [exception]; + AuthenticationUserFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/check_oidc_is_available_state.dart b/lib/features/login/domain/state/check_oidc_is_available_state.dart index 0cc2439ac9..f826c3e8f8 100644 --- a/lib/features/login/domain/state/check_oidc_is_available_state.dart +++ b/lib/features/login/domain/state/check_oidc_is_available_state.dart @@ -2,13 +2,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:model/oidc/response/oidc_response.dart'; -class CheckOIDCIsAvailableLoading extends LoadingState { - - CheckOIDCIsAvailableLoading(); - - @override - List get props => []; -} +class CheckOIDCIsAvailableLoading extends LoadingState {} class CheckOIDCIsAvailableSuccess extends UIState { final OIDCResponse oidcResponse; @@ -20,10 +14,6 @@ class CheckOIDCIsAvailableSuccess extends UIState { } class CheckOIDCIsAvailableFailure extends FeatureFailure { - final dynamic exception; - - CheckOIDCIsAvailableFailure(this.exception); - @override - List get props => [exception]; + CheckOIDCIsAvailableFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/delete_authority_oidc_state.dart b/lib/features/login/domain/state/delete_authority_oidc_state.dart index 946e9658fd..0956c0f496 100644 --- a/lib/features/login/domain/state/delete_authority_oidc_state.dart +++ b/lib/features/login/domain/state/delete_authority_oidc_state.dart @@ -1,17 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; -class DeleteAuthorityOidcSuccess extends UIState { - DeleteAuthorityOidcSuccess(); - - @override - List get props => []; -} +class DeleteAuthorityOidcSuccess extends UIState {} class DeleteAuthorityOidcFailure extends FeatureFailure { - final dynamic exception; - - DeleteAuthorityOidcFailure(this.exception); - @override - List get props => [exception]; + DeleteAuthorityOidcFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/delete_credential_state.dart b/lib/features/login/domain/state/delete_credential_state.dart index d9211905d5..f7b15817b6 100644 --- a/lib/features/login/domain/state/delete_credential_state.dart +++ b/lib/features/login/domain/state/delete_credential_state.dart @@ -1,17 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; -class DeleteCredentialSuccess extends UIState { - DeleteCredentialSuccess(); - - @override - List get props => []; -} +class DeleteCredentialSuccess extends UIState {} class DeleteCredentialFailure extends FeatureFailure { - final exception; - - DeleteCredentialFailure(this.exception); - @override - List get props => [exception]; + DeleteCredentialFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_all_recent_login_url_latest_state.dart b/lib/features/login/domain/state/get_all_recent_login_url_latest_state.dart index 3033f4b839..e9c4c372b7 100644 --- a/lib/features/login/domain/state/get_all_recent_login_url_latest_state.dart +++ b/lib/features/login/domain/state/get_all_recent_login_url_latest_state.dart @@ -13,10 +13,6 @@ class GetAllRecentLoginUrlLatestSuccess extends UIState { } class GetAllRecentLoginUrlLatestFailure extends FeatureFailure { - final dynamic exception; - GetAllRecentLoginUrlLatestFailure(this.exception); - - @override - List get props => [exception]; + GetAllRecentLoginUrlLatestFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_all_recent_login_username_state.dart b/lib/features/login/domain/state/get_all_recent_login_username_state.dart index f8d305e974..9e07ff2be4 100644 --- a/lib/features/login/domain/state/get_all_recent_login_username_state.dart +++ b/lib/features/login/domain/state/get_all_recent_login_username_state.dart @@ -13,10 +13,6 @@ class GetAllRecentLoginUsernameLatestSuccess extends UIState { } class GetAllRecentLoginUsernameLatestFailure extends FeatureFailure { - final dynamic exception; - GetAllRecentLoginUsernameLatestFailure(this.exception); - - @override - List get props => [exception]; + GetAllRecentLoginUsernameLatestFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_authenticated_account_state.dart b/lib/features/login/domain/state/get_authenticated_account_state.dart index fc23bd7b89..89319696f5 100644 --- a/lib/features/login/domain/state/get_authenticated_account_state.dart +++ b/lib/features/login/domain/state/get_authenticated_account_state.dart @@ -1,17 +1,9 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:model/account/account.dart'; - -class NoAuthenticatedAccountFailure extends FeatureFailure { - - NoAuthenticatedAccountFailure(); - - @override - List get props => []; -} +import 'package:model/account/personal_account.dart'; class GetAuthenticatedAccountSuccess extends UIState { - final Account account; + final PersonalAccount account; GetAuthenticatedAccountSuccess(this.account); @@ -20,10 +12,6 @@ class GetAuthenticatedAccountSuccess extends UIState { } class GetAuthenticatedAccountFailure extends FeatureFailure { - final dynamic exception; - - GetAuthenticatedAccountFailure(this.exception); - @override - List get props => [exception]; + GetAuthenticatedAccountFailure(dynamic exception) : super(exception: exception); } diff --git a/lib/features/login/domain/state/get_authentication_info_state.dart b/lib/features/login/domain/state/get_authentication_info_state.dart index cbebcc4175..a9310c3ffc 100644 --- a/lib/features/login/domain/state/get_authentication_info_state.dart +++ b/lib/features/login/domain/state/get_authentication_info_state.dart @@ -1,28 +1,11 @@ - import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -class GetAuthenticationInfoLoading extends LoadingState { - - GetAuthenticationInfoLoading(); - - @override - List get props => []; -} +class GetAuthenticationInfoLoading extends LoadingState {} -class GetAuthenticationInfoSuccess extends UIState { - - GetAuthenticationInfoSuccess(); - - @override - List get props => []; -} +class GetAuthenticationInfoSuccess extends UIState {} class GetAuthenticationInfoFailure extends FeatureFailure { - final dynamic exception; - - GetAuthenticationInfoFailure(this.exception); - @override - List get props => [exception]; + GetAuthenticationInfoFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_credential_state.dart b/lib/features/login/domain/state/get_credential_state.dart index 10d719908f..49c4ab7e2d 100644 --- a/lib/features/login/domain/state/get_credential_state.dart +++ b/lib/features/login/domain/state/get_credential_state.dart @@ -1,6 +1,7 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/password.dart'; -import 'package:model/account/user_name.dart'; class GetCredentialViewState extends UIState { final Uri baseUrl; @@ -10,14 +11,10 @@ class GetCredentialViewState extends UIState { GetCredentialViewState(this.baseUrl, this.userName, this.password); @override - List get props => [baseUrl, this.userName, this.password]; + List get props => [baseUrl, userName, password]; } class GetCredentialFailure extends FeatureFailure { - final exception; - GetCredentialFailure(this.exception); - - @override - List get props => [exception]; + GetCredentialFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_oidc_configuration_state.dart b/lib/features/login/domain/state/get_oidc_configuration_state.dart index 8b00294a04..af9f16b982 100644 --- a/lib/features/login/domain/state/get_oidc_configuration_state.dart +++ b/lib/features/login/domain/state/get_oidc_configuration_state.dart @@ -1,13 +1,8 @@ -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/oidc/oidc_configuration.dart'; -class GetOIDCConfigurationLoading extends LoadingState { - - GetOIDCConfigurationLoading(); - - @override - List get props => []; -} +class GetOIDCConfigurationLoading extends LoadingState {} class GetOIDCConfigurationSuccess extends UIState { @@ -20,10 +15,6 @@ class GetOIDCConfigurationSuccess extends UIState { } class GetOIDCConfigurationFailure extends FeatureFailure { - final dynamic exception; - - GetOIDCConfigurationFailure(this.exception); - @override - List get props => [exception]; + GetOIDCConfigurationFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_oidc_is_available_state.dart b/lib/features/login/domain/state/get_oidc_is_available_state.dart index 7b455456e4..16ac225cfd 100644 --- a/lib/features/login/domain/state/get_oidc_is_available_state.dart +++ b/lib/features/login/domain/state/get_oidc_is_available_state.dart @@ -2,13 +2,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:model/oidc/response/oidc_response.dart'; -class GetOIDCIsAvailableLoading extends LoadingState { - - GetOIDCIsAvailableLoading(); - - @override - List get props => []; -} +class GetOIDCIsAvailableLoading extends LoadingState {} class GetOIDCIsAvailableSuccess extends UIState { final OIDCResponse oidcResponse; @@ -20,10 +14,6 @@ class GetOIDCIsAvailableSuccess extends UIState { } class GetOIDCIsAvailableFailure extends FeatureFailure { - final dynamic exception; - - GetOIDCIsAvailableFailure(this.exception); - @override - List get props => [exception]; + GetOIDCIsAvailableFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_stored_oidc_configuration_state.dart b/lib/features/login/domain/state/get_stored_oidc_configuration_state.dart index f35ae2402e..9f739fdaca 100644 --- a/lib/features/login/domain/state/get_stored_oidc_configuration_state.dart +++ b/lib/features/login/domain/state/get_stored_oidc_configuration_state.dart @@ -2,13 +2,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:model/oidc/oidc_configuration.dart'; -class GetStoredOidcConfigurationLoading extends LoadingState { - - GetStoredOidcConfigurationLoading(); - - @override - List get props => []; -} +class GetStoredOidcConfigurationLoading extends LoadingState {} class GetStoredOidcConfigurationSuccess extends UIState { final OIDCConfiguration oidcConfiguration; @@ -20,10 +14,6 @@ class GetStoredOidcConfigurationSuccess extends UIState { } class GetStoredOidcConfigurationFailure extends FeatureFailure { - final dynamic exception; - - GetStoredOidcConfigurationFailure(this.exception); - @override - List get props => [exception]; + GetStoredOidcConfigurationFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_stored_token_oidc_state.dart b/lib/features/login/domain/state/get_stored_token_oidc_state.dart index 958f74d36c..a8d30356dc 100644 --- a/lib/features/login/domain/state/get_stored_token_oidc_state.dart +++ b/lib/features/login/domain/state/get_stored_token_oidc_state.dart @@ -15,13 +15,6 @@ class GetStoredTokenOidcSuccess extends UIState { } class GetStoredTokenOidcFailure extends FeatureFailure { - final dynamic exception; - GetStoredTokenOidcFailure(this.exception); - - @override - bool? get stringify => true; - - @override - List get props => [exception]; + GetStoredTokenOidcFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_token_oidc_state.dart b/lib/features/login/domain/state/get_token_oidc_state.dart index 6a26c31e01..53d72d9412 100644 --- a/lib/features/login/domain/state/get_token_oidc_state.dart +++ b/lib/features/login/domain/state/get_token_oidc_state.dart @@ -1,13 +1,9 @@ -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/token_oidc.dart'; -class GetTokenOIDCLoading extends LoadingState { - - GetTokenOIDCLoading(); - - @override - List get props => []; -} +class GetTokenOIDCLoading extends LoadingState {} class GetTokenOIDCSuccess extends UIState { @@ -21,10 +17,6 @@ class GetTokenOIDCSuccess extends UIState { } class GetTokenOIDCFailure extends FeatureFailure { - final dynamic exception; - - GetTokenOIDCFailure(this.exception); - @override - List get props => [exception]; + GetTokenOIDCFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/refresh_token_oidc_state.dart b/lib/features/login/domain/state/refresh_token_oidc_state.dart index 55228f9d3d..7680d7c456 100644 --- a/lib/features/login/domain/state/refresh_token_oidc_state.dart +++ b/lib/features/login/domain/state/refresh_token_oidc_state.dart @@ -1,5 +1,6 @@ -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/oidc/token_oidc.dart'; class RefreshTokenOIDCSuccess extends UIState { @@ -12,10 +13,6 @@ class RefreshTokenOIDCSuccess extends UIState { } class RefreshTokenOIDCFailure extends FeatureFailure { - final dynamic exception; - RefreshTokenOIDCFailure(this.exception); - - @override - List get props => [exception]; + RefreshTokenOIDCFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/save_recent_login_url_state.dart b/lib/features/login/domain/state/save_recent_login_url_state.dart index 4932b9ac79..6086ade134 100644 --- a/lib/features/login/domain/state/save_recent_login_url_state.dart +++ b/lib/features/login/domain/state/save_recent_login_url_state.dart @@ -1,20 +1,9 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -class SaveRecentLoginUrlSuccess extends UIState { - - SaveRecentLoginUrlSuccess(); - - @override - List get props => []; -} +class SaveRecentLoginUrlSuccess extends UIState {} class SaveRecentLoginUrlFailed extends FeatureFailure { - final dynamic exception; - - SaveRecentLoginUrlFailed(this.exception); - - @override - List get props => [exception]; + SaveRecentLoginUrlFailed(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/save_recent_login_username_state.dart b/lib/features/login/domain/state/save_recent_login_username_state.dart index 5ae227914c..8fdcf4a90a 100644 --- a/lib/features/login/domain/state/save_recent_login_username_state.dart +++ b/lib/features/login/domain/state/save_recent_login_username_state.dart @@ -1,20 +1,9 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -class SaveRecentLoginUsernameSuccess extends UIState { - - SaveRecentLoginUsernameSuccess(); - - @override - List get props => []; -} +class SaveRecentLoginUsernameSuccess extends UIState {} class SaveRecentLoginUsernameFailed extends FeatureFailure { - final dynamic exception; - - SaveRecentLoginUsernameFailed(this.exception); - - @override - List get props => [exception]; + SaveRecentLoginUsernameFailed(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/state/update_authentication_account_state.dart b/lib/features/login/domain/state/update_authentication_account_state.dart index b0aa0540fa..c5d1931edf 100644 --- a/lib/features/login/domain/state/update_authentication_account_state.dart +++ b/lib/features/login/domain/state/update_authentication_account_state.dart @@ -3,19 +3,9 @@ import 'package:core/presentation/state/success.dart'; class UpdateAuthenticationAccountLoading extends LoadingState {} -class UpdateAuthenticationAccountSuccess extends UIState { - - UpdateAuthenticationAccountSuccess(); - - @override - List get props => []; -} +class UpdateAuthenticationAccountSuccess extends UIState {} class UpdateAuthenticationAccountFailure extends FeatureFailure { - final dynamic exception; - UpdateAuthenticationAccountFailure(this.exception); - - @override - List get props => [exception]; + UpdateAuthenticationAccountFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/login/domain/usecases/authentication_user_interactor.dart b/lib/features/login/domain/usecases/authentication_user_interactor.dart index 6e1b14573a..5e698e41bb 100644 --- a/lib/features/login/domain/usecases/authentication_user_interactor.dart +++ b/lib/features/login/domain/usecases/authentication_user_interactor.dart @@ -1,7 +1,13 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; -import 'package:model/model.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/authentication_type.dart'; +import 'package:model/account/password.dart'; +import 'package:model/account/personal_account.dart'; import 'package:tmail_ui_user/features/login/data/model/authentication_info_cache.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; @@ -18,21 +24,31 @@ class AuthenticationInteractor { this._accountRepository ); - Stream> execute(Uri baseUrl, UserName userName, Password password) async* { + Stream> execute({Uri? baseUrl, UserName? userName, Password? password}) async* { try { yield Right(AuthenticationUserLoading()); - final user = await authenticationRepository.authenticationUser(baseUrl, userName, password); - await Future.wait([ - credentialRepository.saveBaseUrl(baseUrl), - credentialRepository.storeAuthenticationInfo( - AuthenticationInfoCache(userName.userName, password.value)), - _accountRepository.setCurrentAccount(Account( - userName.userName, - AuthenticationType.basic, - isSelected: true - )) - ]); - yield Right(AuthenticationUserSuccess(user)); + + if (baseUrl != null && userName != null && password != null) { + final user = await authenticationRepository.authenticationUser(baseUrl, userName, password); + await Future.wait([ + credentialRepository.saveBaseUrl(baseUrl), + credentialRepository.storeAuthenticationInfo(AuthenticationInfoCache(userName.value, password.value)), + _accountRepository.setCurrentAccount(PersonalAccount( + userName.value, + AuthenticationType.basic, + isSelected: true + )) + ]); + yield Right(AuthenticationUserSuccess(user)); + } else if (baseUrl == null) { + yield Left(AuthenticationUserFailure(CanNotFoundBaseUrl())); + } else if (userName == null) { + yield Left(AuthenticationUserFailure(CanNotFoundUserName())); + } else if (password == null) { + yield Left(AuthenticationUserFailure(CanNotFoundPassword())); + } else { + yield Left(AuthenticationUserFailure(null)); + } } catch (e) { logError('AuthenticationInteractor::execute(): $e'); yield Left(AuthenticationUserFailure(e)); diff --git a/lib/features/login/domain/usecases/get_authenticated_account_interactor.dart b/lib/features/login/domain/usecases/get_authenticated_account_interactor.dart index de2eab1b76..71eb479cde 100644 --- a/lib/features/login/domain/usecases/get_authenticated_account_interactor.dart +++ b/lib/features/login/domain/usecases/get_authenticated_account_interactor.dart @@ -1,9 +1,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:model/account/authentication_type.dart'; -import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_authenticated_account_state.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_credential_interactor.dart'; @@ -24,7 +22,6 @@ class GetAuthenticatedAccountInteractor { try { yield Right(LoadingState()); final account = await _accountRepository.getCurrentAccount(); - log('GetAuthenticatedAccountInteractor::execute(): account: $account'); yield Right(GetAuthenticatedAccountSuccess(account)); if (account.authenticationType == AuthenticationType.oidc) { yield* _getStoredTokenOidcInteractor.execute(account.id); @@ -32,12 +29,7 @@ class GetAuthenticatedAccountInteractor { yield await _getCredentialInteractor.execute(); } } catch (e) { - logError('GetAuthenticatedAccountInteractor::execute(): $e'); - if (e is NotFoundAuthenticatedAccountException) { - yield Left(NoAuthenticatedAccountFailure()); - } else { - yield Left(GetAuthenticatedAccountFailure(e)); - } + yield Left(GetAuthenticatedAccountFailure(e)); } } } \ No newline at end of file diff --git a/lib/features/login/domain/usecases/get_authentication_info_interactor.dart b/lib/features/login/domain/usecases/get_authentication_info_interactor.dart index e33c8d7077..75286c6405 100644 --- a/lib/features/login/domain/usecases/get_authentication_info_interactor.dart +++ b/lib/features/login/domain/usecases/get_authentication_info_interactor.dart @@ -15,11 +15,7 @@ class GetAuthenticationInfoInteractor { yield Right(GetAuthenticationInfoLoading()); final result = await _oidcRepository.getAuthenticationInfo(); log('GetAuthenticationInfoInteractor::execute(): result: $result'); - if (result?.isNotEmpty == true) { - yield Right(GetAuthenticationInfoSuccess()); - } else { - yield Left(GetAuthenticationInfoFailure(null)); - } + yield Right(GetAuthenticationInfoSuccess()); } catch (e) { log('GetAuthenticationInfoInteractor::execute(): ERROR: $e'); yield Left(GetAuthenticationInfoFailure(e)); diff --git a/lib/features/login/domain/usecases/get_credential_interactor.dart b/lib/features/login/domain/usecases/get_credential_interactor.dart index 50d283fe5e..b2dd3f903b 100644 --- a/lib/features/login/domain/usecases/get_credential_interactor.dart +++ b/lib/features/login/domain/usecases/get_credential_interactor.dart @@ -1,8 +1,10 @@ import 'dart:core'; -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; -import 'package:model/model.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/password.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/login/domain/extensions/uri_extension.dart'; import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; @@ -19,9 +21,9 @@ class GetCredentialInteractor { final authenticationInfo = await credentialRepository.getAuthenticationInfoStored(); if (isCredentialValid(baseUrl) && authenticationInfo != null) { return Right(GetCredentialViewState( - baseUrl, - UserName(authenticationInfo.username), - Password(authenticationInfo.password))); + baseUrl, + UserName(authenticationInfo.username), + Password(authenticationInfo.password))); } else { return Left(GetCredentialFailure(BadCredentials())); } diff --git a/lib/features/login/domain/usecases/get_token_oidc_interactor.dart b/lib/features/login/domain/usecases/get_token_oidc_interactor.dart index c44b4d885f..9455aac2fe 100644 --- a/lib/features/login/domain/usecases/get_token_oidc_interactor.dart +++ b/lib/features/login/domain/usecases/get_token_oidc_interactor.dart @@ -3,8 +3,8 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; -import 'package:model/account/account.dart'; import 'package:model/account/authentication_type.dart'; +import 'package:model/account/personal_account.dart'; import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart'; @@ -31,10 +31,10 @@ class GetTokenOIDCInteractor { config.scopes); await Future.wait([ _credentialRepository.saveBaseUrl(baseUrl), - _accountRepository.setCurrentAccount(Account( - tokenOIDC.tokenIdHash, - AuthenticationType.oidc, - isSelected: true)), + _accountRepository.setCurrentAccount(PersonalAccount( + tokenOIDC.tokenIdHash, + AuthenticationType.oidc, + isSelected: true)), authenticationOIDCRepository.persistTokenOIDC(tokenOIDC), authenticationOIDCRepository.persistAuthorityOidc(config.authority), ]); diff --git a/lib/features/login/domain/usecases/save_login_url_on_mobile_interactor.dart b/lib/features/login/domain/usecases/save_login_url_on_mobile_interactor.dart index 9655a1e1a4..235f366a7f 100644 --- a/lib/features/login/domain/usecases/save_login_url_on_mobile_interactor.dart +++ b/lib/features/login/domain/usecases/save_login_url_on_mobile_interactor.dart @@ -10,12 +10,12 @@ class SaveLoginUrlOnMobileInteractor { SaveLoginUrlOnMobileInteractor(this.loginUrlRepository); - Stream> execute(RecentLoginUrl recentLoginUrl) async* { + Future> execute(RecentLoginUrl recentLoginUrl) async { try{ await loginUrlRepository.saveRecentLoginUrl(recentLoginUrl); - yield Right(SaveRecentLoginUrlSuccess()); + return Right(SaveRecentLoginUrlSuccess()); } catch(e) { - yield Left(SaveRecentLoginUrlFailed(e)); + return Left(SaveRecentLoginUrlFailed(e)); } } } \ No newline at end of file diff --git a/lib/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart b/lib/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart index dc29054f02..41597ba252 100644 --- a/lib/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart +++ b/lib/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart @@ -9,12 +9,12 @@ class SaveLoginUsernameOnMobileInteractor { SaveLoginUsernameOnMobileInteractor(this.loginUsernameRepository); - Stream> execute(RecentLoginUsername recentLoginUsername) async* { + Future> execute(RecentLoginUsername recentLoginUsername) async { try { await loginUsernameRepository.saveLoginUsername(recentLoginUsername); - yield Right(SaveRecentLoginUsernameSuccess()); + return Right(SaveRecentLoginUsernameSuccess()); } catch(exception) { - yield Left(SaveRecentLoginUsernameFailed(exception)); + return Left(SaveRecentLoginUsernameFailed(exception)); } } } \ No newline at end of file diff --git a/lib/features/login/domain/usecases/update_authentication_account_interactor.dart b/lib/features/login/domain/usecases/update_authentication_account_interactor.dart index dc5a55d701..d12cfd696a 100644 --- a/lib/features/login/domain/usecases/update_authentication_account_interactor.dart +++ b/lib/features/login/domain/usecases/update_authentication_account_interactor.dart @@ -2,7 +2,8 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:model/extensions/account_extension.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/extensions/personal_account_extension.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; import 'package:tmail_ui_user/features/login/domain/state/update_authentication_account_state.dart'; @@ -11,14 +12,15 @@ class UpdateAuthenticationAccountInteractor { UpdateAuthenticationAccountInteractor(this._accountRepository); - Stream> execute(AccountId accountId, String apiUrl) async* { + Stream> execute(AccountId accountId, String apiUrl, UserName userName) async* { try{ yield Right(UpdateAuthenticationAccountLoading()); final currentAccount = await _accountRepository.getCurrentAccount(); await _accountRepository.setCurrentAccount( - currentAccount.fromAccountId( + currentAccount.fromAccount( accountId: accountId, - apiUrl: apiUrl + apiUrl: apiUrl, + userName: userName ) ); yield Right(UpdateAuthenticationAccountSuccess()); diff --git a/lib/features/login/presentation/base_login_view.dart b/lib/features/login/presentation/base_login_view.dart index a8cf6c459e..86ea753a7d 100644 --- a/lib/features/login/presentation/base_login_view.dart +++ b/lib/features/login/presentation/base_login_view.dart @@ -1,17 +1,25 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/presentation/views/text/text_builder.dart'; +import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/widget/recent_item_tile_widget.dart'; +import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/login/domain/model/recent_login_username.dart'; +import 'package:tmail_ui_user/features/login/domain/state/authenticate_oidc_on_browser_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/authentication_user_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/check_oidc_is_available_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_oidc_configuration_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_oidc_is_available_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_token_oidc_state.dart'; import 'package:tmail_ui_user/features/login/presentation/login_controller.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; -import 'package:tmail_ui_user/features/login/presentation/state/login_state.dart'; import 'package:tmail_ui_user/features/login/presentation/widgets/login_input_decoration_builder.dart'; import 'package:tmail_ui_user/features/login/presentation/widgets/login_text_input_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -19,144 +27,147 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; abstract class BaseLoginView extends GetWidget { BaseLoginView({Key? key}) : super(key: key); - final loginController = Get.find(); final responsiveUtils = Get.find(); final imagePaths = Get.find(); - final FocusNode passFocusNode = FocusNode(); - - Widget buildLoginMessage(BuildContext context, LoginState loginState) { + Widget buildLoginMessage(BuildContext context, Either viewState) { return Padding( padding: const EdgeInsets.only(top: 11, bottom: 36, left: 58, right: 58), child: SizedBox( width: responsiveUtils.getWidthLoginTextField(context), - child: CenterTextBuilder() - .key(const Key('login_message')) - .text(loginState.viewState.fold( + child: Text( + viewState.fold( (failure) { - if (failure is LoginMissUrlAction) { - return AppLocalizations.of(context).requiredUrl; - } else if (failure is LoginMissUsernameAction) { - return AppLocalizations.of(context).requiredEmail; - } else if (failure is LoginMissPasswordAction) { - return AppLocalizations.of(context).requiredPassword; - } else if (failure is LoginSSONotAvailableAction) { - return AppLocalizations.of(context).ssoNotAvailable; - } else if (failure is GetOIDCConfigurationFailure - || failure is LoginCanNotVerifySSOConfigurationAction) { + if (failure is CheckOIDCIsAvailableFailure) { + return _getMessageFailure(context, failure.exception); + } else if (failure is AuthenticationUserFailure) { + return _getMessageFailure(context, failure.exception); + } else if (failure is GetOIDCIsAvailableFailure) { + return _getMessageFailure(context, failure.exception); + } else if (failure is GetTokenOIDCFailure) { + return _getMessageFailure(context, failure.exception); + } else if (failure is AuthenticateOidcOnBrowserFailure) { + return _getMessageFailure(context, failure.exception); + } else if (failure is GetOIDCConfigurationFailure) { return AppLocalizations.of(context).canNotVerifySSOConfiguration; - } else if (failure is GetTokenOIDCFailure || failure is LoginCanNotGetTokenAction) { - return AppLocalizations.of(context).canNotGetToken; } else { return AppLocalizations.of(context).unknownError; } }, (success) { - if (loginController.loginFormType.value == LoginFormType.credentialForm) { + if (controller.loginFormType.value == LoginFormType.credentialForm) { return AppLocalizations.of(context).loginInputCredentialMessage; - } else if (loginController.loginFormType.value == LoginFormType.ssoForm) { + } else if (controller.loginFormType.value == LoginFormType.ssoForm) { return AppLocalizations.of(context).loginInputSSOMessage; } return AppLocalizations.of(context).loginInputUrlMessage; - })) - .textStyle(TextStyle( - fontSize: 15, - fontWeight: FontWeight.w400, - color: loginState.viewState.fold( - (failure) => AppColor.textFieldErrorBorderColor, - (success) => AppColor.colorNameEmail))) - .build() - ) + }), + key: const Key('login_message'), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + color: viewState.fold( + (failure) => AppColor.textFieldErrorBorderColor, + (success) => AppColor.colorNameEmail)), + )) ); } + String _getMessageFailure(BuildContext context, dynamic exception) { + if (exception is CanNotFoundBaseUrl) { + return AppLocalizations.of(context).requiredUrl; + } else if (exception is CanNotFoundUserName) { + return AppLocalizations.of(context).requiredEmail; + } else if (exception is CanNotFoundPassword) { + return AppLocalizations.of(context).requiredPassword; + } else if (exception is CanNotFoundOIDCLinks) { + return AppLocalizations.of(context).ssoNotAvailable; + } else if (exception is CanNotFoundToken) { + return AppLocalizations.of(context).canNotGetToken; + } else { + return ''; + } + } + Widget buildLoginButton(BuildContext context) { return Container( - margin: const EdgeInsets.only(bottom: 16, left: 24, right: 24), - width: responsiveUtils.getDeviceWidth(context),height: 48, - child: AbsorbPointer( - absorbing: !controller.networkConnectionController.isNetworkConnectionAvailable(), - child: ElevatedButton( - key: const Key('loginSubmitForm'), - style: ButtonStyle( - foregroundColor: MaterialStateProperty.resolveWith((Set states) => Colors.white), - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) => AppColor.primaryColor.withOpacity( - controller.networkConnectionController.isNetworkConnectionAvailable() ? 1 : 0.5)), - shape: MaterialStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - side: const BorderSide(width: 0, color: AppColor.primaryColor) - )) - ), - child: Text(AppLocalizations.of(context).signIn, - style: const TextStyle(fontSize: 16, color: Colors.white) - ), - onPressed: () { - loginController.handleLoginPressed(); - } - ), + margin: const EdgeInsets.only(bottom: 16, left: 24, right: 24), + width: responsiveUtils.getDeviceWidth(context),height: 48, + child: ElevatedButton( + key: const Key('loginSubmitForm'), + style: ButtonStyle( + foregroundColor: MaterialStateProperty.resolveWith((Set states) => Colors.white), + backgroundColor: MaterialStateProperty.resolveWith((Set states) => AppColor.primaryColor), + shape: MaterialStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: const BorderSide(width: 0, color: AppColor.primaryColor) + )) + ), + onPressed: controller.handleLoginPressed, + child: Text( + AppLocalizations.of(context).signIn, + style: const TextStyle(fontSize: 16, color: Colors.white) ) + ) ); } Widget buildInputCredentialForm(BuildContext context) { - return Column( - children: [ - buildUserNameInput(context), - buildPasswordInput(context) - ], + return AutofillGroup( + child: Padding( + padding: const EdgeInsetsDirectional.symmetric(horizontal: 24), + child: Column( + children: [ + buildUserNameInput(context), + const SizedBox(height: 24), + buildPasswordInput(context), + const SizedBox(height: 40), + ], + ), + ), ); } Widget buildUserNameInput(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 24, right: 24, left: 24), - child: TypeAheadFormField( - key: const Key('login_username_input'), - textFieldConfiguration: TextFieldConfiguration( - controller: loginController.usernameInputController, - onChanged: (value) => loginController.setUserNameText(value), - textInputAction: TextInputAction.next, - keyboardType: TextInputType.emailAddress, - decoration: (LoginInputDecorationBuilder() - ..setLabelText(AppLocalizations.of(context).email) - ..setHintText(AppLocalizations.of(context).email)) - .build(), - ), - debounceDuration: const Duration(milliseconds: 300), - suggestionsCallback: (pattern) async { - return await loginController.getAllRecentLoginUsernameAction(pattern); - }, - itemBuilder: (context, loginUsername) => - RecentItemTileWidget(loginUsername, imagePath: imagePaths), - onSuggestionSelected: (recentUsername) { - loginController.setUsername(recentUsername.username); - passFocusNode.requestFocus(); - }, - suggestionsBoxDecoration: const SuggestionsBoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(14))), - noItemsFoundBuilder: (context) => const SizedBox(), - hideOnEmpty: true, - hideOnError: true, - hideOnLoading: true, - ) + return TypeAheadFormFieldBuilder( + key: const Key('login_username_input'), + controller: controller.usernameInputController, + onTextChange: controller.setUserNameText, + textInputAction: TextInputAction.next, + autocorrect: false, + autofillHints: const [AutofillHints.email], + keyboardType: TextInputType.emailAddress, + decoration: (LoginInputDecorationBuilder() + ..setLabelText(AppLocalizations.of(context).email) + ..setHintText(AppLocalizations.of(context).email)) + .build(), + debounceDuration: const Duration(milliseconds: 300), + suggestionsCallback: controller.getAllRecentLoginUsernameAction, + itemBuilder: (context, loginUsername) => RecentItemTileWidget(loginUsername, imagePath: imagePaths), + onSuggestionSelected: (recentUsername) { + controller.setUsername(recentUsername.username); + controller.passFocusNode.requestFocus(); + }, + suggestionsBoxDecoration: const SuggestionsBoxDecoration(borderRadius: BorderRadius.all(Radius.circular(14))), + noItemsFoundBuilder: (context) => const SizedBox(), + hideOnEmpty: true, + hideOnError: true, + hideOnLoading: true, ); } Widget buildPasswordInput(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(bottom: 40, right: 24, left: 24), - child: Container( - child: (LoginTextInputBuilder(context, imagePaths) - ..setOnSubmitted((value) => loginController.handleLoginPressed()) - ..passwordInput(true) - ..key(const Key('login_password_input')) - ..obscureText(true) - ..onChange((value) => loginController.setPasswordText(value)) - ..textInputAction(TextInputAction.done) - ..hintText(AppLocalizations.of(context).password) - ..setFocusNode(passFocusNode)) - .build())); + return LoginTextInputBuilder( + key: const Key('login_password_input'), + controller: controller.passwordInputController, + autofillHints: const [AutofillHints.password], + textInputAction: TextInputAction.done, + hintText: AppLocalizations.of(context).password, + focusNode: controller.passFocusNode, + onTextChange: controller.setPasswordText, + onSubmitted: (value) => controller.handleLoginPressed(), + ); } Widget buildLoadingCircularProgress() { diff --git a/lib/features/login/presentation/login_bindings.dart b/lib/features/login/presentation/login_bindings.dart index af3979e0de..ef34ac5ae0 100644 --- a/lib/features/login/presentation/login_bindings.dart +++ b/lib/features/login/presentation/login_bindings.dart @@ -1,90 +1,61 @@ import 'package:core/core.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/caching/recent_login_url_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/recent_login_username_cache_client.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/account_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/authentication_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_login_url_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_login_username_cache_client.dart'; import 'package:tmail_ui_user/features/login/data/datasource/login_url_datasource.dart'; import 'package:tmail_ui_user/features/login/data/datasource/login_username_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/hive_account_datasource_impl.dart'; import 'package:tmail_ui_user/features/login/data/datasource_impl/login_url_datasource_impl.dart'; import 'package:tmail_ui_user/features/login/data/datasource_impl/login_username_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; -import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; -import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; -import 'package:tmail_ui_user/features/login/data/repository/account_repository_impl.dart'; -import 'package:tmail_ui_user/features/login/data/repository/authentication_oidc_repository_impl.dart'; -import 'package:tmail_ui_user/features/login/data/repository/authentication_repository_impl.dart'; import 'package:tmail_ui_user/features/login/data/repository/login_url_repository_impl.dart'; import 'package:tmail_ui_user/features/login/data/repository/login_username_repository_impl.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/authentication_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/login_url_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/login_username_repository.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/authenticate_oidc_on_browser_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_all_recent_login_url_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_all_recent_login_username_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authentication_info_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/get_credential_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_oidc_configuration_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_oidc_is_available_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_oidc_configuration_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_token_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/save_login_url_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/save_login_username_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_controller.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; -import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; -import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; class LoginBindings extends BaseBindings { @override void bindingsController() { - Get.lazyPut(() => LoginController( - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(tag: BindingTag.isolateTag), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), + Get.create(() => LoginController( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), )); } @override void bindingsDataSource() { - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); @@ -92,17 +63,6 @@ class LoginBindings extends BaseBindings { @override void bindingsDataSourceImpl() { - Get.lazyPut(() => AuthenticationDataSourceImpl()); - Get.lazyPut(() => AuthenticationOIDCDataSourceImpl( - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find() - )); - Get.lazyPut(() => HiveAccountDatasourceImpl( - Get.find(), - Get.find())); Get.lazyPut(() => LoginUrlDataSourceImpl( Get.find(), Get.find())); @@ -113,28 +73,6 @@ class LoginBindings extends BaseBindings { @override void bindingsInteractor() { - Get.lazyPut(() => LogoutOidcInteractor( - Get.find(), - Get.find(), - )); - Get.lazyPut(() => DeleteAuthorityOidcInteractor( - Get.find(), - Get.find()) - ); - Get.lazyPut(() => GetStoredTokenOidcInteractor( - Get.find(), - Get.find(), - )); - Get.lazyPut(() => GetAuthenticatedAccountInteractor( - Get.find(), - Get.find(), - Get.find(), - )); - Get.lazyPut(() => AuthenticationInteractor( - Get.find(), - Get.find(), - Get.find() - )); Get.lazyPut(() => CheckOIDCIsAvailableInteractor( Get.find(), )); @@ -173,9 +111,6 @@ class LoginBindings extends BaseBindings { @override void bindingsRepository() { - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); @@ -183,9 +118,6 @@ class LoginBindings extends BaseBindings { @override void bindingsRepositoryImpl() { - Get.lazyPut(() => AuthenticationRepositoryImpl(Get.find())); - Get.lazyPut(() => AuthenticationOIDCRepositoryImpl(Get.find())); - Get.lazyPut(() => AccountRepositoryImpl(Get.find())); Get.lazyPut(() => LoginUrlRepositoryImpl(Get.find())); Get.lazyPut(() => LoginUsernameRepositoryImpl(Get.find())); } diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart index 56ffa5c9ae..469cb48d48 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -3,20 +3,22 @@ import 'package:core/presentation/extensions/url_extension.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/password.dart'; -import 'package:model/account/user_name.dart'; import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/request/oidc_request.dart'; import 'package:model/oidc/response/oidc_response.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; -import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; +import 'package:tmail_ui_user/features/login/data/network/oidc_error.dart'; +import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; +import 'package:tmail_ui_user/features/login/domain/model/login_constants.dart'; import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart'; import 'package:tmail_ui_user/features/login/domain/model/recent_login_username.dart'; import 'package:tmail_ui_user/features/login/domain/state/authenticate_oidc_on_browser_state.dart'; @@ -24,6 +26,7 @@ import 'package:tmail_ui_user/features/login/domain/state/authentication_user_st import 'package:tmail_ui_user/features/login/domain/state/check_oidc_is_available_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_all_recent_login_url_latest_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_all_recent_login_username_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_authenticated_account_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_authentication_info_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_oidc_configuration_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_oidc_is_available_state.dart'; @@ -32,7 +35,6 @@ import 'package:tmail_ui_user/features/login/domain/state/get_token_oidc_state.d import 'package:tmail_ui_user/features/login/domain/usecases/authenticate_oidc_on_browser_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_all_recent_login_url_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_all_recent_login_username_on_mobile_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; @@ -46,21 +48,17 @@ import 'package:tmail_ui_user/features/login/domain/usecases/save_login_username import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/model/login_arguments.dart'; -import 'package:tmail_ui_user/features/login/presentation/state/login_state.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; -import 'package:tmail_ui_user/features/network_status_handle/presentation/network_connnection_controller.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/navigation_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; +import 'package:universal_html/html.dart' as html; class LoginController extends ReloadableController { final AuthenticationInteractor _authenticationInteractor; final DynamicUrlInterceptors _dynamicUrlInterceptors; - final AuthorizationInterceptors _authorizationInterceptors; - final AuthorizationInterceptors _authorizationIsolateInterceptors; final CheckOIDCIsAvailableInteractor _checkOIDCIsAvailableInteractor; final GetOIDCIsAvailableInteractor _getOIDCIsAvailableInteractor; final GetOIDCConfigurationInteractor _getOIDCConfigurationInteractor; @@ -73,20 +71,16 @@ class LoginController extends ReloadableController { final SaveLoginUsernameOnMobileInteractor _saveLoginUsernameOnMobileInteractor; final GetAllRecentLoginUsernameOnMobileInteractor _getAllRecentLoginUsernameOnMobileInteractor; - final TextEditingController urlInputController = TextEditingController(); final TextEditingController usernameInputController = TextEditingController(); - final NetworkConnectionController networkConnectionController = Get.find(); + final TextEditingController passwordInputController = TextEditingController(); + final FocusNode passFocusNode = FocusNode(); LoginController( - LogoutOidcInteractor logoutOidcInteractor, - DeleteAuthorityOidcInteractor deleteAuthorityOidcInteractor, GetAuthenticatedAccountInteractor getAuthenticatedAccountInteractor, UpdateAuthenticationAccountInteractor updateAuthenticationAccountInteractor, this._authenticationInteractor, this._dynamicUrlInterceptors, - this._authorizationInterceptors, - this._authorizationIsolateInterceptors, this._checkOIDCIsAvailableInteractor, this._getOIDCIsAvailableInteractor, this._getOIDCConfigurationInteractor, @@ -99,13 +93,10 @@ class LoginController extends ReloadableController { this._saveLoginUsernameOnMobileInteractor, this._getAllRecentLoginUsernameOnMobileInteractor, ) : super( - logoutOidcInteractor, - deleteAuthorityOidcInteractor, getAuthenticatedAccountInteractor, updateAuthenticationAccountInteractor ); - var loginState = LoginState(Right(LoginInitAction())).obs; final loginFormType = LoginFormType.baseUrlForm.obs; String? _urlText; @@ -134,7 +125,7 @@ class LoginController extends ReloadableController { @override void onReady() { super.onReady(); - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { final arguments = Get.arguments; if (arguments is LoginArguments) { loginFormType.value = arguments.loginFormType; @@ -146,61 +137,56 @@ class LoginController extends ReloadableController { } @override - void onData(Either newState) { - super.onData(newState); - viewState.value.fold( - (failure) { - if (failure is GetAuthenticationInfoFailure) { - getAuthenticatedAccountAction(); - } else if (failure is CheckOIDCIsAvailableFailure || - failure is GetStoredOidcConfigurationFailure) { - _showFormLoginWithCredentialAction(); - } else if (failure is GetOIDCIsAvailableFailure) { - loginState.value = LoginState(Left(LoginSSONotAvailableAction())); - _showFormLoginWithCredentialAction(); - } else if (failure is AuthenticationUserFailure) { - _loginFailureAction(failure); - } else if (failure is GetOIDCConfigurationFailure || - failure is GetTokenOIDCFailure || - failure is AuthenticateOidcOnBrowserFailure) { - loginState.value = LoginState(Left(failure)); - } - }, - (success) { - if (success is GetAuthenticationInfoSuccess) { - _getStoredOidcConfiguration(); - } else if (success is GetStoredOidcConfigurationSuccess) { - _getTokenOIDCAction(success.oidcConfiguration); - } else if (success is CheckOIDCIsAvailableSuccess) { - _showFormLoginWithSSOAction(success); - } else if (success is GetOIDCIsAvailableSuccess) { - loginState.value = LoginState(Right(success)); - _oidcResponse = success.oidcResponse; - _getOIDCConfiguration(); - } else if (success is GetOIDCConfigurationSuccess) { - _getOIDCConfigurationSuccess(success); - } else if (success is GetTokenOIDCSuccess) { - _getTokenOIDCSuccess(success); - } else if (success is AuthenticationUserSuccess) { - _loginSuccessAction(success); - } else if (success is GetAuthenticationInfoLoading || - success is CheckOIDCIsAvailableLoading || - success is GetStoredOidcConfigurationLoading || - success is GetOIDCConfigurationLoading || - success is GetTokenOIDCLoading || - success is AuthenticationUserLoading || - success is GetOIDCIsAvailableLoading) { - loginState.value = LoginState(Right(LoginLoadingAction())); - } - } - ); + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is GetAuthenticationInfoFailure) { + getAuthenticatedAccountAction(); + } else if (failure is CheckOIDCIsAvailableFailure || + failure is GetStoredOidcConfigurationFailure || + failure is GetOIDCIsAvailableFailure) { + _showFormLoginWithCredentialAction(); + } else if (failure is GetAuthenticatedAccountFailure) { + _checkOIDCIsAvailable(); + } + } + + @override + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetAuthenticationInfoSuccess) { + _getStoredOidcConfiguration(); + } else if (success is GetStoredOidcConfigurationSuccess) { + _getTokenOIDCAction(success.oidcConfiguration); + } else if (success is CheckOIDCIsAvailableSuccess) { + _redirectToSSOLoginScreen(success); + } else if (success is GetOIDCIsAvailableSuccess) { + _oidcResponse = success.oidcResponse; + _getOIDCConfiguration(); + } else if (success is GetOIDCConfigurationSuccess) { + _getOIDCConfigurationSuccess(success); + } else if (success is GetTokenOIDCSuccess) { + _getTokenOIDCSuccess(success); + } else if (success is AuthenticationUserSuccess) { + _loginSuccessAction(success); + } + } + + @override + void handleExceptionAction({Failure? failure, Exception? exception}) { + super.handleExceptionAction(failure: failure, exception: exception); + if (failure is CheckOIDCIsAvailableFailure || failure is GetOIDCIsAvailableFailure) { + _showFormLoginWithCredentialAction(); + } else { + clearState(); + } } @override void handleReloaded(Session session) { - pushAndPop( + popAndPush( RouteUtils.generateNavigationRoute(AppRoutes.dashboard, NavigationRouter()), - arguments: session); + arguments: session + ); } void _getAuthenticationInfo() { @@ -217,61 +203,61 @@ class LoginController extends ReloadableController { } void _checkOIDCIsAvailable() { - final baseUri = BuildUtils.isWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText); - if (baseUri == null) { - loginState.value = LoginState(Left(LoginMissUrlAction())); + final baseUrl = _getBaseUrl(); + if (baseUrl == null) { + dispatchState(Left(CheckOIDCIsAvailableFailure(CanNotFoundBaseUrl()))); } else { - consumeState(_checkOIDCIsAvailableInteractor.execute(OIDCRequest( - baseUrl: baseUri.toString(), - resourceUrl: baseUri.origin))); + consumeState(_checkOIDCIsAvailableInteractor.execute( + OIDCRequest( + baseUrl: baseUrl.toString(), + resourceUrl: baseUrl.origin + ) + )); } } - void _showFormLoginWithSSOAction(CheckOIDCIsAvailableSuccess success) { - loginState.value = LoginState(Right(success)); - loginFormType.value = LoginFormType.ssoForm; + void _redirectToSSOLoginScreen(CheckOIDCIsAvailableSuccess success) { _oidcResponse = success.oidcResponse; + handleSSOPressed(); } void handleBackInCredentialForm() { - loginState.value = LoginState(Right(LoginInitAction())); + clearState(); loginFormType.value = LoginFormType.baseUrlForm; } void _showFormLoginWithCredentialAction() { - loginState.value = LoginState(Right(InputUrlCompletion())); + clearState(); loginFormType.value = LoginFormType.credentialForm; } void handleLoginPressed() { + TextInput.finishAutofillContext(); + _saveRecentLoginUsername(); log('LoginController::handleLoginPressed(): ${loginFormType.value}'); if (loginFormType.value == LoginFormType.ssoForm) { _getOIDCConfiguration(); } else { - final baseUri = kIsWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText); + final baseUrl = _getBaseUrl(); final userName = _parseUserName(_userNameText); final password = _parsePassword(_passwordText); - if (baseUri != null && userName != null && password != null) { - _loginAction(baseUri, userName, password); - } else if (baseUri == null) { - loginState.value = LoginState(Left(LoginMissUrlAction())); - } else if (userName == null) { - loginState.value = LoginState(Left(LoginMissUsernameAction())); - } else if (password == null) { - loginState.value = LoginState(Left(LoginMissPasswordAction())); - } + + _loginAction(baseUrl: baseUrl, userName: userName, password: password); } } void handleSSOPressed() { - final baseUri = _parseUri(AppConfig.baseUrl); - if (baseUri != null) { - consumeState(_getOIDCIsAvailableInteractor.execute(OIDCRequest( - baseUrl: baseUri.toString(), - resourceUrl: baseUri.origin))); - } else { - loginState.value = LoginState(Left(LoginCanNotAuthenticationSSOAction())); + final baseUrl = _getBaseUrl(); + if (baseUrl != null) { + consumeState(_getOIDCIsAvailableInteractor.execute( + OIDCRequest( + baseUrl: baseUrl.toString(), + resourceUrl: baseUrl.origin + ) + )); + } else { + dispatchState(Left(GetOIDCIsAvailableFailure(CanNotFoundBaseUrl()))); } } @@ -279,13 +265,13 @@ class LoginController extends ReloadableController { if (_oidcResponse != null) { consumeState(_getOIDCConfigurationInteractor.execute(_oidcResponse!)); } else { - loginState.value = LoginState(Left(LoginCanNotAuthenticationSSOAction())); + dispatchState(Left(GetOIDCIsAvailableFailure(CanNotFoundOIDCLinks()))); } } void _getOIDCConfigurationSuccess(GetOIDCConfigurationSuccess success) { - loginState.value = LoginState(Right(success)); - if (BuildUtils.isWeb) { + log('LoginController::_getOIDCConfigurationSuccess():success: $success'); + if (PlatformInfo.isWeb) { _authenticateOidcOnBrowserAction(success.oidcConfiguration); } else { _getTokenOIDCAction(success.oidcConfiguration); @@ -293,51 +279,59 @@ class LoginController extends ReloadableController { } void _getTokenOIDCAction(OIDCConfiguration config) async { - final baseUri = kIsWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText); + final baseUri = _getBaseUrl(); if (baseUri != null) { - consumeState(_getTokenOIDCInteractor.execute(baseUri, config)); + consumeState(_getTokenOIDCInteractor.execute(baseUri, config)); } else { - loginState.value = LoginState(Left(LoginCanNotGetTokenAction())); + dispatchState(Left(GetTokenOIDCFailure(CanNotFoundBaseUrl()))); } } void _authenticateOidcOnBrowserAction(OIDCConfiguration config) async { + _removeAuthDestinationUrlInSessionStorage(); + final baseUri = _parseUri(AppConfig.baseUrl); if (baseUri != null) { consumeState(_authenticateOidcOnBrowserInteractor.execute(baseUri, config)); } else { - loginState.value = LoginState(Left(LoginCanNotAuthenticationSSOAction())); + dispatchState(Left(AuthenticateOidcOnBrowserFailure(CanNotFoundBaseUrl()))); + } + } + + void _removeAuthDestinationUrlInSessionStorage() { + final authDestinationUrlExist = html.window.sessionStorage.containsKey(LoginConstants.AUTH_DESTINATION_KEY); + if (authDestinationUrlExist) { + html.window.sessionStorage.remove(LoginConstants.AUTH_DESTINATION_KEY); } } void _getTokenOIDCSuccess(GetTokenOIDCSuccess success) { log('LoginController::_getTokenOIDCSuccess(): ${success.tokenOIDC.toString()}'); - loginState.value = LoginState(Right(success)); - _dynamicUrlInterceptors.changeBaseUrl(kIsWeb ? AppConfig.baseUrl : _urlText); - _authorizationInterceptors.setTokenAndAuthorityOidc( + _dynamicUrlInterceptors.setJmapUrl(_getBaseUrl().toString()); + _dynamicUrlInterceptors.changeBaseUrl(_getBaseUrl().toString()); + authorizationInterceptors.setTokenAndAuthorityOidc( newToken: success.tokenOIDC.toToken(), newConfig: success.configuration); - _authorizationIsolateInterceptors.setTokenAndAuthorityOidc( + authorizationIsolateInterceptors.setTokenAndAuthorityOidc( newToken: success.tokenOIDC.toToken(), newConfig: success.configuration); - pushAndPop(AppRoutes.session, arguments: _dynamicUrlInterceptors.baseUrl); + popAndPush(AppRoutes.session, arguments: _dynamicUrlInterceptors.baseUrl); } - void _loginAction(Uri baseUrl, UserName userName, Password password) async { - consumeState(_authenticationInteractor.execute(baseUrl, userName, password)); + void _loginAction({Uri? baseUrl, UserName? userName, Password? password}) { + consumeState(_authenticationInteractor.execute( + baseUrl: baseUrl, + userName: userName, + password: password + )); } void _loginSuccessAction(AuthenticationUserSuccess success) { - loginState.value = LoginState(Right(success)); - _dynamicUrlInterceptors.changeBaseUrl(kIsWeb ? AppConfig.baseUrl : _urlText); - _authorizationInterceptors.setBasicAuthorization(_userNameText, _passwordText); - _authorizationIsolateInterceptors.setBasicAuthorization(_userNameText, _passwordText); - pushAndPop(AppRoutes.session, arguments: _dynamicUrlInterceptors.baseUrl); - } - - void _loginFailureAction(FeatureFailure failure) { - logError('LoginController::_loginFailureAction(): $failure'); - loginState.value = LoginState(Left(failure)); + _dynamicUrlInterceptors.setJmapUrl(_getBaseUrl().toString()); + _dynamicUrlInterceptors.changeBaseUrl(_getBaseUrl().toString()); + authorizationInterceptors.setBasicAuthorization(_userNameText, _passwordText); + authorizationIsolateInterceptors.setBasicAuthorization(_userNameText, _passwordText); + popAndPush(AppRoutes.session, arguments: _dynamicUrlInterceptors.baseUrl); } void formatUrl(String url) { @@ -349,10 +343,10 @@ class LoginController extends ReloadableController { } void _saveRecentLoginUrl() { - if (_urlText?.isNotEmpty == true && !BuildUtils.isWeb) { + if (_urlText?.isNotEmpty == true && PlatformInfo.isMobile) { final recentLoginUrl = RecentLoginUrl.now(_urlText!); log('LoginController::_saveRecentLoginUrl(): $recentLoginUrl'); - consumeState(_saveLoginUrlOnMobileInteractor.execute(recentLoginUrl)); + _saveLoginUrlOnMobileInteractor.execute(recentLoginUrl); } } @@ -374,12 +368,12 @@ class LoginController extends ReloadableController { } void _saveRecentLoginUsername() { - if(BuildUtils.isWeb || _userNameText == null || _userNameText!.isEmpty || !_userNameText!.isEmail) { + if(PlatformInfo.isWeb || _userNameText == null || _userNameText!.isEmpty || !_userNameText!.isEmail) { return ; } final recentLoginUsername = RecentLoginUsername.now(_userNameText!); log('LoginController::_saveRecentLoginUsername(): $recentLoginUsername'); - consumeState(_saveLoginUsernameOnMobileInteractor.execute(recentLoginUsername)); + _saveLoginUsernameOnMobileInteractor.execute(recentLoginUsername); } Future> getAllRecentLoginUsernameAction(String pattern) async { @@ -393,12 +387,14 @@ class LoginController extends ReloadableController { )); } + Uri? _getBaseUrl() => PlatformInfo.isWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText); + @override void onClose() { - urlInputController.clear(); + passFocusNode.dispose(); + urlInputController.dispose(); + usernameInputController.dispose(); + passwordInputController.dispose(); super.onClose(); } - - @override - void onDone() {} } \ No newline at end of file diff --git a/lib/features/login/presentation/login_view.dart b/lib/features/login/presentation/login_view.dart index 3107738d58..983b7c19bd 100644 --- a/lib/features/login/presentation/login_view.dart +++ b/lib/features/login/presentation/login_view.dart @@ -1,4 +1,6 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/views/text/type_ahead_form_field_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; @@ -8,14 +10,11 @@ import 'package:tmail_ui_user/features/login/domain/model/recent_login_url.dart' import 'package:tmail_ui_user/features/login/presentation/base_login_view.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/privacy_link_widget.dart'; -import 'package:tmail_ui_user/features/login/presentation/state/login_state.dart'; import 'package:tmail_ui_user/features/login/presentation/widgets/login_input_decoration_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class LoginView extends BaseLoginView { - final keyboardUtils = Get.find(); - LoginView({Key? key}) : super(key: key); @override @@ -30,11 +29,11 @@ class LoginView extends BaseLoginView { child: _supportScrollForm(context) ? Stack(children: [ Center(child: SingleChildScrollView( - child: _buildCenterForm(context), - scrollDirection: Axis.vertical)), + scrollDirection: Axis.vertical, + child: _buildCenterForm(context))), Obx(() { - if (loginController.loginFormType.value == LoginFormType.credentialForm - || loginController.loginFormType.value == LoginFormType.ssoForm) { + if (controller.loginFormType.value == LoginFormType.credentialForm + || controller.loginFormType.value == LoginFormType.ssoForm) { return _buildBackButton(context); } return const SizedBox.shrink(); @@ -43,8 +42,8 @@ class LoginView extends BaseLoginView { : Stack(children: [ _buildCenterForm(context), Obx(() { - if (loginController.loginFormType.value == LoginFormType.credentialForm - || loginController.loginFormType.value == LoginFormType.ssoForm) { + if (controller.loginFormType.value == LoginFormType.credentialForm + || controller.loginFormType.value == LoginFormType.ssoForm) { return _buildBackButton(context); } return const SizedBox.shrink(); @@ -73,7 +72,7 @@ class LoginView extends BaseLoginView { style: const TextStyle(fontSize: 32, color: AppColor.colorNameEmail, fontWeight: FontWeight.w900) ) ), - Obx(() => buildLoginMessage(context, loginController.loginState.value)), + Obx(() => buildLoginMessage(context, controller.viewState.value)), Obx(() { switch (controller.loginFormType.value) { case LoginFormType.baseUrlForm: @@ -84,30 +83,7 @@ class LoginView extends BaseLoginView { return const SizedBox.shrink(); } }), - Obx(() { - switch (controller.loginFormType.value) { - case LoginFormType.baseUrlForm: - return Obx(() => loginController.loginState.value.viewState.fold( - (failure) => _buildNextButtonInContext(context), - (success) => success is LoginLoadingAction - ? buildLoadingCircularProgress() - : _buildNextButtonInContext(context))); - case LoginFormType.credentialForm: - return Obx(() => loginController.loginState.value.viewState.fold( - (failure) => _buildLoginButtonInContext(context), - (success) => success is LoginLoadingAction - ? buildLoadingCircularProgress() - : _buildLoginButtonInContext(context))); - case LoginFormType.ssoForm: - return Obx(() => loginController.loginState.value.viewState.fold( - (failure) => _buildLoginButtonInContext(context), - (success) => success is LoginLoadingAction - ? buildLoadingCircularProgress() - : _buildLoginButtonInContext(context))); - default: - return const SizedBox.shrink(); - } - }), + _buildLoadingProgress(context), const Padding( padding: EdgeInsets.only(top: 16), child: PrivacyLinkWidget(), @@ -128,7 +104,7 @@ class LoginView extends BaseLoginView { icon: SvgPicture.asset( imagePaths.icBack, alignment: Alignment.center, - color: AppColor.primaryColor + colorFilter: AppColor.primaryColor.asFilter() ) ), ); @@ -137,27 +113,23 @@ class LoginView extends BaseLoginView { Widget _buildUrlInput(BuildContext context) { return Padding( padding: const EdgeInsets.only(right: 24, left: 24, bottom: 24), - child: TypeAheadFormField( - textFieldConfiguration: TextFieldConfiguration( - controller: loginController.urlInputController, - textInputAction: TextInputAction.next, - keyboardType: TextInputType.url, - onSubmitted: (value) => controller.handleNextInUrlInputFormPress(), - decoration: (LoginInputDecorationBuilder() - ..setLabelText(AppLocalizations.of(context).prefix_https) - ..setPrefixText(AppLocalizations.of(context).prefix_https)) - .build() - ), + child: TypeAheadFormFieldBuilder( + controller: controller.urlInputController, + textInputAction: TextInputAction.next, + keyboardType: TextInputType.url, + onTextSubmitted: (value) => controller.handleNextInUrlInputFormPress(), + decoration: (LoginInputDecorationBuilder() + ..setLabelText(AppLocalizations.of(context).prefix_https) + ..setPrefixText(AppLocalizations.of(context).prefix_https)) + .build(), debounceDuration: const Duration(milliseconds: 300), suggestionsCallback: (pattern) async { - loginController.formatUrl(pattern); - return loginController.getAllRecentLoginUrlAction(pattern); + controller.formatUrl(pattern); + return controller.getAllRecentLoginUrlAction(pattern); }, - itemBuilder: (context, loginUrl) => - RecentItemTileWidget(loginUrl, imagePath: imagePaths), + itemBuilder: (context, loginUrl) => RecentItemTileWidget(loginUrl, imagePath: imagePaths), onSuggestionSelected: (loginUrl) => controller.formatUrl(loginUrl.url), - suggestionsBoxDecoration: const SuggestionsBoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(14))), + suggestionsBoxDecoration: const SuggestionsBoxDecoration(borderRadius: BorderRadius.all(Radius.circular(14))), noItemsFoundBuilder: (context) => const SizedBox(), hideOnEmpty: true, hideOnError: true, @@ -178,27 +150,23 @@ class LoginView extends BaseLoginView { return Container( margin: const EdgeInsets.only(bottom: 16, left: 24, right: 24), width: responsiveUtils.getDeviceWidth(context),height: 48, - child: AbsorbPointer( - absorbing: !controller.networkConnectionController.isNetworkConnectionAvailable(), - child: ElevatedButton( - key: const Key('nextToCredentialForm'), - style: ButtonStyle( - foregroundColor: MaterialStateProperty.resolveWith((Set states) => Colors.white), - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) => AppColor.primaryColor.withOpacity( - controller.networkConnectionController.isNetworkConnectionAvailable() ? 1 : 0.5)), - shape: MaterialStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - side: const BorderSide(width: 0, color: AppColor.primaryColor) - )) - ), - child: Text(AppLocalizations.of(context).next, - style: const TextStyle(fontSize: 16, color: Colors.white) - ), - onPressed: () { - loginController.handleNextInUrlInputFormPress(); - } + child: ElevatedButton( + key: const Key('nextToCredentialForm'), + style: ButtonStyle( + foregroundColor: MaterialStateProperty.resolveWith((Set states) => Colors.white), + backgroundColor: MaterialStateProperty.resolveWith( + (Set states) => AppColor.primaryColor), + shape: MaterialStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: const BorderSide(width: 0, color: AppColor.primaryColor) + )) ), + child: Text(AppLocalizations.of(context).next, + style: const TextStyle(fontSize: 16, color: Colors.white) + ), + onPressed: () { + controller.handleNextInUrlInputFormPress(); + } ) ); } @@ -218,4 +186,37 @@ class LoginView extends BaseLoginView { bool _supportScrollForm(BuildContext context) { return !(responsiveUtils.isMobile(context) && responsiveUtils.isPortrait(context)); } + + Widget _buildLoadingProgress(BuildContext context) { + return Obx(() => controller.viewState.value.fold( + (failure) { + switch (controller.loginFormType.value) { + case LoginFormType.baseUrlForm: + return _buildNextButtonInContext(context); + case LoginFormType.credentialForm: + return _buildLoginButtonInContext(context); + case LoginFormType.ssoForm: + return _buildLoginButtonInContext(context); + default: + return const SizedBox.shrink(); + } + }, + (success) { + if (success is LoadingState) { + return buildLoadingCircularProgress(); + } else { + switch (controller.loginFormType.value) { + case LoginFormType.baseUrlForm: + return _buildNextButtonInContext(context); + case LoginFormType.credentialForm: + return _buildLoginButtonInContext(context); + case LoginFormType.ssoForm: + return _buildLoginButtonInContext(context); + default: + return const SizedBox.shrink(); + } + } + } + )); + } } \ No newline at end of file diff --git a/lib/features/login/presentation/login_view_web.dart b/lib/features/login/presentation/login_view_web.dart index 603bb11afd..4905fc5835 100644 --- a/lib/features/login/presentation/login_view_web.dart +++ b/lib/features/login/presentation/login_view_web.dart @@ -1,11 +1,13 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/views/responsive/responsive_widget.dart'; +import 'package:core/presentation/views/text/slogan_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get_state_manager/src/rx_flutter/rx_obx_widget.dart'; import 'package:tmail_ui_user/features/login/presentation/base_login_view.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/privacy_link_widget.dart'; -import 'package:tmail_ui_user/features/login/presentation/state/login_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class LoginView extends BaseLoginView { @@ -45,7 +47,7 @@ class LoginView extends BaseLoginView { style: const TextStyle(fontSize: 32, color: AppColor.colorNameEmail, fontWeight: FontWeight.w900) ) ), - Obx(() => buildLoginMessage(context, loginController.loginState.value)), + Obx(() => buildLoginMessage(context, controller.viewState.value)), Obx(() { switch (controller.loginFormType.value) { case LoginFormType.credentialForm: @@ -56,30 +58,7 @@ class LoginView extends BaseLoginView { return const SizedBox.shrink(); } }), - Obx(() { - switch (controller.loginFormType.value) { - case LoginFormType.baseUrlForm: - return Obx(() => loginController.loginState.value.viewState.fold( - (failure) => const SizedBox.shrink(), - (success) => success is LoginLoadingAction - ? buildLoadingCircularProgress() - : const SizedBox.shrink())); - case LoginFormType.credentialForm: - return Obx(() => loginController.loginState.value.viewState.fold( - (failure) => buildLoginButton(context), - (success) => success is LoginLoadingAction - ? buildLoadingCircularProgress() - : buildLoginButton(context))); - case LoginFormType.ssoForm: - return Obx(() => loginController.loginState.value.viewState.fold( - (failure) => _buildSSOButton(context), - (success) => success is LoginLoadingAction - ? buildLoadingCircularProgress() - : _buildSSOButton(context))); - default: - return const SizedBox.shrink(); - } - }), + _buildLoadingProgress(context), const Padding( padding: EdgeInsets.only(top: 16), child: PrivacyLinkWidget(), @@ -120,48 +99,52 @@ class LoginView extends BaseLoginView { ), Padding( padding: const EdgeInsets.only(top: 24), - child: (SloganBuilder(arrangedByHorizontal: true, ) - ..setLogoSVG(imagePaths.icJMAPStandard) - ..setSizeLogo(48.0) - ..setPadding(const EdgeInsets.only(left: 12)) - ..setSloganText(AppLocalizations.of(context).jmapStandard) - ..setSloganTextStyle(const TextStyle(fontSize: 24, fontWeight: FontWeight.w400, color: AppColor.colorNameEmail))) - .build() + child: SloganBuilder( + arrangedByHorizontal: true, + logoSVG: imagePaths.icJMAPStandard, + sizeLogo: 48.0, + paddingText: const EdgeInsets.only(left: 12), + text: AppLocalizations.of(context).jmapStandard, + textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.w400, color: AppColor.colorNameEmail) + ) ), Padding( padding: const EdgeInsets.only(top: 16), - child: (SloganBuilder(arrangedByHorizontal: true, ) - ..setLogoSVG(imagePaths.icEncrypted) - ..setSizeLogo(48.0) - ..setPadding(const EdgeInsets.only(left: 12)) - ..setSloganText(AppLocalizations.of(context).encryptedMailbox) - ..setSloganTextStyle(const TextStyle(fontSize: 24, fontWeight: FontWeight.w400, color: AppColor.colorNameEmail))) - .build() + child: SloganBuilder( + arrangedByHorizontal: true, + logoSVG: imagePaths.icEncrypted, + sizeLogo: 48.0, + paddingText: const EdgeInsets.only(left: 12), + text: AppLocalizations.of(context).encryptedMailbox, + textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.w400, color: AppColor.colorNameEmail) + ) ), Padding( padding: const EdgeInsets.only(top: 16), - child: (SloganBuilder(arrangedByHorizontal: true, ) - ..setLogoSVG(imagePaths.icTeam) - ..setSizeLogo(48.0) - ..setPadding(const EdgeInsets.only(left: 12)) - ..setSloganText(AppLocalizations.of(context).manageEmailAsATeam) - ..setSloganTextStyle(const TextStyle(fontSize: 24, fontWeight: FontWeight.w400, color: AppColor.colorNameEmail))) - .build() + child: SloganBuilder( + arrangedByHorizontal: true, + logoSVG: imagePaths.icTeam, + sizeLogo: 48.0, + paddingText: const EdgeInsets.only(left: 12), + text: AppLocalizations.of(context).manageEmailAsATeam, + textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.w400, color: AppColor.colorNameEmail) + ) ), Padding( padding: const EdgeInsets.only(top: 16), - child: (SloganBuilder(arrangedByHorizontal: true, ) - ..setLogoSVG(imagePaths.icIntegration) - ..setSizeLogo(48.0) - ..setPadding(const EdgeInsets.only(left: 12)) - ..setSloganText(AppLocalizations.of(context).multipleIntegrations) - ..setSloganTextStyle(const TextStyle(fontSize: 24, fontWeight: FontWeight.w400, color: AppColor.colorNameEmail))) - .build() + child: SloganBuilder( + arrangedByHorizontal: true, + logoSVG: imagePaths.icIntegration, + sizeLogo: 48.0, + paddingText: const EdgeInsets.only(left: 12), + text: AppLocalizations.of(context).multipleIntegrations, + textStyle: const TextStyle(fontSize: 24, fontWeight: FontWeight.w400, color: AppColor.colorNameEmail) + ) ), Padding( padding: const EdgeInsets.only(top: 44), - child: Image( - image: AssetImage(imagePaths.loginGraphic), + child: SvgPicture.asset( + imagePaths.icLoginGraphic, fit: BoxFit.fill, alignment: Alignment.center ) @@ -196,7 +179,7 @@ class LoginView extends BaseLoginView { style: const TextStyle(fontSize: 32, color: AppColor.colorNameEmail, fontWeight: FontWeight.w900) ) ), - Obx(() => buildLoginMessage(context, loginController.loginState.value)), + Obx(() => buildLoginMessage(context, controller.viewState.value)), Obx(() { switch (controller.loginFormType.value) { case LoginFormType.credentialForm: @@ -207,30 +190,7 @@ class LoginView extends BaseLoginView { return const SizedBox.shrink(); } }), - Obx(() { - switch (controller.loginFormType.value) { - case LoginFormType.baseUrlForm: - return Obx(() => loginController.loginState.value.viewState.fold( - (failure) => const SizedBox.shrink(), - (success) => success is LoginLoadingAction - ? buildLoadingCircularProgress() - : const SizedBox.shrink())); - case LoginFormType.credentialForm: - return Obx(() => loginController.loginState.value.viewState.fold( - (failure) => buildLoginButton(context), - (success) => success is LoginLoadingAction - ? buildLoadingCircularProgress() - : buildLoginButton(context))); - case LoginFormType.ssoForm: - return Obx(() => loginController.loginState.value.viewState.fold( - (failure) => _buildSSOButton(context), - (success) => success is LoginLoadingAction - ? buildLoadingCircularProgress() - : _buildSSOButton(context))); - default: - return const SizedBox.shrink(); - } - }), + _buildLoadingProgress(context), const Padding( padding: EdgeInsets.only(top: 16), child: PrivacyLinkWidget() @@ -255,8 +215,8 @@ class LoginView extends BaseLoginView { return Row( mainAxisSize: MainAxisSize.min, children: [ - Image( - image: AssetImage(imagePaths.icLogoTMail), + SvgPicture.asset( + imagePaths.icTMailLogo, fit: BoxFit.fill, width: 36, height: 36, @@ -276,28 +236,57 @@ class LoginView extends BaseLoginView { return Container( margin: const EdgeInsets.only(bottom: 16, left: 24, right: 24), width: responsiveUtils.getDeviceWidth(context),height: 48, - child: AbsorbPointer( - absorbing: !controller.networkConnectionController.isNetworkConnectionAvailable(), - child: ElevatedButton( - key: const Key('ssoSubmitForm'), - style: ButtonStyle( - foregroundColor: MaterialStateProperty.resolveWith((Set states) => Colors.white), - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) => AppColor.primaryColor.withOpacity( - controller.networkConnectionController.isNetworkConnectionAvailable() ? 1 : 0.5)), - shape: MaterialStateProperty.all(RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - side: const BorderSide(width: 0, color: AppColor.primaryColor) - )) - ), - child: Text(AppLocalizations.of(context).singleSignOn, - style: const TextStyle(fontSize: 16, color: Colors.white) - ), - onPressed: () { - loginController.handleSSOPressed(); - } - ), + child: ElevatedButton( + key: const Key('ssoSubmitForm'), + style: ButtonStyle( + foregroundColor: MaterialStateProperty.resolveWith((Set states) => Colors.white), + backgroundColor: MaterialStateProperty.resolveWith( + (Set states) => AppColor.primaryColor), + shape: MaterialStateProperty.all(RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: const BorderSide(width: 0, color: AppColor.primaryColor) + )) + ), + child: Text(AppLocalizations.of(context).singleSignOn, + style: const TextStyle(fontSize: 16, color: Colors.white) + ), + onPressed: () { + controller.handleSSOPressed(); + } ) ); } + + Widget _buildLoadingProgress(BuildContext context) { + return Obx(() => controller.viewState.value.fold( + (failure) { + switch (controller.loginFormType.value) { + case LoginFormType.baseUrlForm: + return const SizedBox.shrink(); + case LoginFormType.credentialForm: + return buildLoginButton(context); + case LoginFormType.ssoForm: + return _buildSSOButton(context); + default: + return const SizedBox.shrink(); + } + }, + (success) { + if (success is LoadingState) { + return buildLoadingCircularProgress(); + } else { + switch (controller.loginFormType.value) { + case LoginFormType.baseUrlForm: + return const SizedBox.shrink(); + case LoginFormType.credentialForm: + return buildLoginButton(context); + case LoginFormType.ssoForm: + return _buildSSOButton(context); + default: + return const SizedBox.shrink(); + } + } + } + )); + } } \ No newline at end of file diff --git a/lib/features/login/presentation/privacy_link_widget.dart b/lib/features/login/presentation/privacy_link_widget.dart index c6c05ac5db..2969c838cb 100644 --- a/lib/features/login/presentation/privacy_link_widget.dart +++ b/lib/features/login/presentation/privacy_link_widget.dart @@ -2,7 +2,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/widgets.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -import 'package:url_launcher/url_launcher_string.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; class PrivacyLinkWidget extends StatelessWidget { static const linagoraPrivacy = 'https://www.linagora.com/en/legal/privacy'; @@ -19,11 +19,7 @@ class PrivacyLinkWidget extends StatelessWidget { style: const TextStyle( color: AppColor.loginTextFieldFocusedBorder, fontSize: 14), - recognizer: TapGestureRecognizer()..onTap = () async { - if (await canLaunchUrlString(privacyUrlString)) { - launchUrlString(privacyUrlString); - } - } + recognizer: TapGestureRecognizer()..onTap = () => AppUtils.launchLink(privacyUrlString) ) ); } diff --git a/lib/features/login/presentation/state/login_state.dart b/lib/features/login/presentation/state/login_state.dart deleted file mode 100644 index 502563db0b..0000000000 --- a/lib/features/login/presentation/state/login_state.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:core/core.dart'; -import 'package:dartz/dartz.dart'; -import 'package:flutter/material.dart'; - -@immutable -class LoginState extends AppState { - LoginState(Either viewState) : super(viewState); -} - -@immutable -class InputUrlCompletion extends ViewState { - @override - List get props => []; -} - -@immutable -class LoginLoadingAction extends ViewState { - @override - List get props => []; -} - -@immutable -class LoginInitAction extends ViewState { - @override - List get props => []; -} - -@immutable -class LoginMissUrlAction extends Failure { - @override - List get props => []; -} - -@immutable -class LoginMissUsernameAction extends Failure { - @override - List get props => []; -} - -@immutable -class LoginMissPasswordAction extends Failure { - @override - List get props => []; -} - -@immutable -class LoginCanNotVerifySSOConfigurationAction extends Failure { - @override - List get props => []; -} - -@immutable -class LoginCanNotGetTokenAction extends Failure { - @override - List get props => []; -} - -class LoginSSONotAvailableAction extends Failure { - @override - List get props => []; -} - -class LoginCanNotAuthenticationSSOAction extends Failure { - @override - List get props => []; -} \ No newline at end of file diff --git a/lib/features/login/presentation/widgets/login_input_decoration_builder.dart b/lib/features/login/presentation/widgets/login_input_decoration_builder.dart index a741669cac..9261ba0241 100644 --- a/lib/features/login/presentation/widgets/login_input_decoration_builder.dart +++ b/lib/features/login/presentation/widgets/login_input_decoration_builder.dart @@ -22,7 +22,7 @@ class LoginInputDecorationBuilder extends InputDecorationBuilder { labelStyle: labelStyle ?? const TextStyle(color: AppColor.textFieldLabelColor, fontSize: 16), hintText: hintText, hintStyle: hintStyle ?? const TextStyle(color: AppColor.textFieldHintColor, fontSize: 16), - contentPadding: contentPadding ?? const EdgeInsets.only(left: 25, top: 15, bottom: 15, right: 25), + contentPadding: contentPadding ?? const EdgeInsetsDirectional.only(start: 25, top: 15, bottom: 15, end: 25), filled: true, fillColor: AppColor.textFieldBorderColor); } diff --git a/lib/features/login/presentation/widgets/login_text_input_builder.dart b/lib/features/login/presentation/widgets/login_text_input_builder.dart index 514e124650..0189870d39 100644 --- a/lib/features/login/presentation/widgets/login_text_input_builder.dart +++ b/lib/features/login/presentation/widgets/login_text_input_builder.dart @@ -1,166 +1,145 @@ -import 'dart:async'; - -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/presentation/views/text/text_form_field_builder.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -import 'login_input_decoration_builder.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/login/presentation/widgets/login_input_decoration_builder.dart'; -typedef SetErrorString = String? Function(String); typedef OnSubmitted = void Function(String); -class LoginTextInputBuilder { - Key? _key; - String? _title; - String? _hintText; - String? _labelText; - String? _prefixText; - SetErrorString? _setErrorString; - String? _errorText; - Timer? _debounce; - bool? _obscureText; - ValueChanged? _onTextChange; - TextInputAction? _textInputAction; - TextEditingController? _textEditingController; - bool? _passwordInput; - OnSubmitted? _onSubmitted; - FocusNode? _focusNode; - - final BuildContext context; - final ImagePaths imagePaths; - - LoginTextInputBuilder(this.context, this.imagePaths); - - void key(Key key) { - _key = key; - } - - void title(String? title) { - _title = title; - } - - void hintText(String? hintText) { - _hintText = hintText; - } - - void labelText(String? labelText) { - _labelText = labelText; - } - - void prefixText(String? prefixText) { - _prefixText = prefixText; - } - - void textInputAction(TextInputAction inputAction) { - _textInputAction = inputAction; - } - - void obscureText(bool obscureText) { - _obscureText = obscureText; - } - - void onChange(ValueChanged onChange) { - _onTextChange = onChange; - } +class LoginTextInputBuilder extends StatefulWidget { + final String? hintText; + final String? prefixText; + final TextInputAction? textInputAction; + final TextEditingController? controller; + final FocusNode? focusNode; + final List? autofillHints; + final bool obscureText; + final bool passwordInput; + final ValueChanged? onTextChange; + final OnSubmitted? onSubmitted; + + const LoginTextInputBuilder({ + super.key, + this.hintText, + this.prefixText, + this.textInputAction, + this.focusNode, + this.controller, + this.autofillHints, + this.onSubmitted, + this.onTextChange, + this.passwordInput = true, + this.obscureText = true, + }); + + @override + State createState() => _LoginTextInputBuilderState(); +} - void setErrorString(SetErrorString setErrorString) { - _setErrorString = setErrorString; - } +class _LoginTextInputBuilderState extends State { - void passwordInput(bool? passwordInput) { - _passwordInput = passwordInput; - } + final imagePaths = Get.find(); - void setTextEditingController(TextEditingController textEditingController) { - _textEditingController = textEditingController; - } + late TextEditingController _controller; + late bool _obscureText; - void _onTextChanged(String name, StateSetter setState) { - if (_onTextChange != null) { - _onTextChange!(name); + @override + void initState() { + super.initState(); + _obscureText = widget.obscureText; + if (widget.controller != null) { + _controller = widget.controller!; + } else { + _controller = TextEditingController(); } - if (_debounce?.isActive ?? false) _debounce?.cancel(); - _debounce = Timer(const Duration(milliseconds: 500), () { - setState(() { - _errorText = (_setErrorString != null) ? _setErrorString!(name) : ''; - }); - }); - } - - void errorText(String? errorText) { - _errorText = errorText; - } - - void _onObscureTextChanged(bool? value, StateSetter setState) { - setState(() { - _obscureText = value == true ? false : true; - }); - } - - void setOnSubmitted(OnSubmitted onSubmitted) { - _onSubmitted = onSubmitted; - } - - void setFocusNode(FocusNode? focusNode) { - _focusNode = focusNode; } - Widget build() { - return StatefulBuilder(builder: (BuildContext context, StateSetter setState) { - return Wrap( - key: _key, - children: [ - Text(_title ?? '', style: const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 14, fontWeight: FontWeight.normal)), - Padding( - padding: const EdgeInsets.only(top: 8), - child: Stack( - alignment: AlignmentDirectional.centerEnd, - children: [ - TextFormField( - onFieldSubmitted: _onSubmitted, - onChanged: (value) => _onTextChanged(value, setState), - obscureText: _obscureText ?? false, - textInputAction: _textInputAction, - controller: _textEditingController, - cursorColor: AppColor.primaryColor, - style: const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 16, fontWeight: FontWeight.normal), - focusNode: _focusNode, - decoration: (LoginInputDecorationBuilder() - ..setHintText(_hintText) - ..setPrefixText(_prefixText) - ..setErrorText(_errorText) - ..setHintStyle(const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 16, fontWeight: FontWeight.normal)) - ..setPrefixStyle(const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 16, fontWeight: FontWeight.normal)) - ..setErrorTextStyle(const TextStyle(color: AppColor.loginTextFieldErrorBorder, fontSize: 13, fontWeight: FontWeight.normal)) - ..setFocusBorder(const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - borderSide: BorderSide(width: 1, color: AppColor.loginTextFieldFocusedBorder))) - ..setEnabledBorder(const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - borderSide: BorderSide(width: 1, color: AppColor.loginTextFieldBorderColor))) - ..setErrorBorder(const OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - borderSide: BorderSide(width: 1, color: AppColor.loginTextFieldErrorBorder)))) - .build(), - ), - if (_passwordInput == true) - Transform( - transform: Matrix4.translationValues( - 0.0, - (_errorText != null && _errorText!.isNotEmpty) ? -10.0 : 0.0, - 0.0), - child: IconButton( - onPressed: () => _onObscureTextChanged(_obscureText, setState), - icon: SvgPicture.asset( - _obscureText == true ? imagePaths.icEye : imagePaths.icEyeOff, - width: 18, - height: 18, - fit: BoxFit.fill))) - ] - ) + @override + Widget build(BuildContext context) { + return Stack( + alignment: AlignmentDirectional.centerEnd, + children: [ + TextFormFieldBuilder( + onTextSubmitted: widget.onSubmitted, + onTextChange: widget.onTextChange, + obscureText: _obscureText, + textInputAction: widget.textInputAction, + autofillHints: widget.autofillHints, + controller: _controller, + textStyle: const TextStyle( + color: AppColor.loginTextFieldHintColor, + fontSize: 16, + fontWeight: FontWeight.normal ), - ], - ); - }); + focusNode: widget.focusNode, + decoration: (LoginInputDecorationBuilder() + ..setHintText(widget.hintText) + ..setPrefixText(widget.prefixText) + ..setContentPadding(const EdgeInsetsDirectional.only( + start: 25, + top: 15, + bottom: 15, + end: 40 + )) + ..setHintStyle(const TextStyle( + color: AppColor.loginTextFieldHintColor, + fontSize: 16, + fontWeight: FontWeight.normal + )) + ..setPrefixStyle(const TextStyle( + color: AppColor.loginTextFieldHintColor, + fontSize: 16, + fontWeight: FontWeight.normal + )) + ..setErrorTextStyle(const TextStyle( + color: AppColor.loginTextFieldErrorBorder, + fontSize: 13, + fontWeight: FontWeight.normal + )) + ..setFocusBorder(const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + borderSide: BorderSide( + width: 1, + color: AppColor.loginTextFieldFocusedBorder + ) + )) + ..setEnabledBorder(const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + borderSide: BorderSide( + width: 1, + color: AppColor.loginTextFieldBorderColor + ) + )) + ..setErrorBorder(const OutlineInputBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + borderSide: BorderSide( + width: 1, + color: AppColor.loginTextFieldErrorBorder + ) + )) + ).build(), + ), + if (widget.passwordInput) + TMailButtonWidget.fromIcon( + icon: widget.obscureText ? imagePaths.icEye : imagePaths.icEyeOff, + iconSize: 18, + margin: const EdgeInsetsDirectional.only(end: 4), + backgroundColor: Colors.transparent, + onTapActionCallback: () { + setState(() => _obscureText = !_obscureText); + }, + ) + ] + ); + } + + @override + void dispose() { + if (widget.controller == null) { + _controller.dispose(); + } + super.dispose(); } } diff --git a/lib/features/mailbox/data/datasource/mailbox_datasource.dart b/lib/features/mailbox/data/datasource/mailbox_datasource.dart index ddbf23682d..5bff0a9c53 100644 --- a/lib/features/mailbox/data/datasource/mailbox_datasource.dart +++ b/lib/features/mailbox/data/datasource/mailbox_datasource.dart @@ -9,6 +9,7 @@ import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_change_response.dart'; @@ -22,27 +23,28 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_multiple_m abstract class MailboxDataSource { Future getAllMailbox(Session session, AccountId accountId, {Properties? properties}); - Future> getAllMailboxCache(); + Future> getAllMailboxCache(AccountId accountId, UserName userName); Future getChanges(Session session, AccountId accountId, State sinceState); - Future update({List? updated, List? created, List? destroyed}); + Future update(AccountId accountId, UserName userName, {List? updated, List? created, List? destroyed}); - Future createNewMailbox(AccountId accountId, CreateNewMailboxRequest newMailboxRequest); + Future createNewMailbox(Session session, AccountId accountId, CreateNewMailboxRequest newMailboxRequest); Future> deleteMultipleMailbox(Session session, AccountId accountId, List mailboxIds); - Future renameMailbox(AccountId accountId, RenameMailboxRequest request); + Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request); - Future moveMailbox(AccountId accountId, MoveMailboxRequest request); + Future moveMailbox(Session session, AccountId accountId, MoveMailboxRequest request); Future> markAsMailboxRead( + Session session, AccountId accountId, MailboxId mailboxId, int totalEmailUnread, StreamController> onProgressController); - Future subscribeMailbox(AccountId accountId, SubscribeMailboxRequest request); + Future subscribeMailbox(Session session, AccountId accountId, SubscribeMailboxRequest request); - Future> subscribeMultipleMailbox(AccountId accountId, SubscribeMultipleMailboxRequest subscribeRequest); + Future> subscribeMultipleMailbox(Session session, AccountId accountId, SubscribeMultipleMailboxRequest subscribeRequest); } \ No newline at end of file diff --git a/lib/features/mailbox/data/datasource/state_datasource.dart b/lib/features/mailbox/data/datasource/state_datasource.dart index fcce4a1211..27778eb89e 100644 --- a/lib/features/mailbox/data/datasource/state_datasource.dart +++ b/lib/features/mailbox/data/datasource/state_datasource.dart @@ -1,9 +1,11 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/state_cache.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart'; abstract class StateDataSource { - Future getState(StateType stateType); + Future getState(AccountId accountId, UserName userName, StateType stateType); - Future saveState(StateCache stateCache); + Future saveState(AccountId accountId, UserName userName, StateCache stateCache); } \ No newline at end of file diff --git a/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart b/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart index 791d03bd0e..8d831e1e92 100644 --- a/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart +++ b/lib/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart @@ -9,6 +9,7 @@ import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; @@ -40,26 +41,22 @@ class MailboxCacheDataSourceImpl extends MailboxDataSource { } @override - Future update({List? updated, List? created, List? destroyed}) { + Future update(AccountId accountId, UserName userName, {List? updated, List? created, List? destroyed}) { return Future.sync(() async { - return await _mailboxCacheManager.update(updated: updated, created: created, destroyed: destroyed); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await _mailboxCacheManager.update(accountId, userName, updated: updated, created: created, destroyed: destroyed); + }).catchError(_exceptionThrower.throwException); } @override - Future> getAllMailboxCache() { + Future> getAllMailboxCache(AccountId accountId, UserName userName) { return Future.sync(() async { - final listMailboxes = await _mailboxCacheManager.getAllMailbox(); + final listMailboxes = await _mailboxCacheManager.getAllMailbox(accountId, userName); return listMailboxes; - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future createNewMailbox(AccountId accountId, CreateNewMailboxRequest newMailboxRequest) { + Future createNewMailbox(Session session, AccountId accountId, CreateNewMailboxRequest newMailboxRequest) { throw UnimplementedError(); } @@ -69,17 +66,18 @@ class MailboxCacheDataSourceImpl extends MailboxDataSource { } @override - Future renameMailbox(AccountId accountId, RenameMailboxRequest request) { + Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request) { throw UnimplementedError(); } @override - Future moveMailbox(AccountId accountId, MoveMailboxRequest request) { + Future moveMailbox(Session session, AccountId accountId, MoveMailboxRequest request) { throw UnimplementedError(); } @override Future> markAsMailboxRead( + Session session, AccountId accountId, MailboxId mailboxId, int totalEmailUnread, @@ -88,12 +86,12 @@ class MailboxCacheDataSourceImpl extends MailboxDataSource { } @override - Future subscribeMailbox(AccountId accountId, SubscribeMailboxRequest request) { + Future subscribeMailbox(Session session, AccountId accountId, SubscribeMailboxRequest request) { throw UnimplementedError(); } @override - Future> subscribeMultipleMailbox(AccountId accountId, SubscribeMultipleMailboxRequest subscribeRequest) { + Future> subscribeMultipleMailbox(Session session, AccountId accountId, SubscribeMultipleMailboxRequest subscribeRequest) { throw UnimplementedError(); } } \ No newline at end of file diff --git a/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart b/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart index 3b3ca0fcaf..39077ab39c 100644 --- a/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart +++ b/lib/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart @@ -9,6 +9,7 @@ import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; @@ -35,98 +36,86 @@ class MailboxDataSourceImpl extends MailboxDataSource { Future getAllMailbox(Session session, AccountId accountId, {Properties? properties}) { return Future.sync(() async { return await mailboxAPI.getAllMailbox(session, accountId, properties: properties); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future getChanges(Session session, AccountId accountId, State sinceState) { return Future.sync(() async { return await mailboxAPI.getChanges(session, accountId, sinceState); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future update({List? updated, List? created, List? destroyed}) { + Future update(AccountId accountId, UserName userName, {List? updated, List? created, List? destroyed}) { throw UnimplementedError(); } @override - Future> getAllMailboxCache() { + Future> getAllMailboxCache(AccountId accountId, UserName userName) { throw UnimplementedError(); } @override - Future createNewMailbox(AccountId accountId, CreateNewMailboxRequest newMailboxRequest) { + Future createNewMailbox(Session session, AccountId accountId, CreateNewMailboxRequest newMailboxRequest) { return Future.sync(() async { - return await mailboxAPI.createNewMailbox(accountId, newMailboxRequest); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await mailboxAPI.createNewMailbox(session, accountId, newMailboxRequest); + }).catchError(_exceptionThrower.throwException); } @override Future> deleteMultipleMailbox(Session session, AccountId accountId, List mailboxIds) { return Future.sync(() async { return await mailboxAPI.deleteMultipleMailbox(session, accountId, mailboxIds); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future renameMailbox(AccountId accountId, RenameMailboxRequest request) { + Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request) { return Future.sync(() async { - return await mailboxAPI.renameMailbox(accountId, request); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await mailboxAPI.renameMailbox(session, accountId, request); + }).catchError(_exceptionThrower.throwException); } @override - Future moveMailbox(AccountId accountId, MoveMailboxRequest request) { + Future moveMailbox(Session session, AccountId accountId, MoveMailboxRequest request) { return Future.sync(() async { - return await mailboxAPI.moveMailbox(accountId, request); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await mailboxAPI.moveMailbox(session, accountId, request); + }).catchError(_exceptionThrower.throwException); } @override Future> markAsMailboxRead( + Session session, AccountId accountId, MailboxId mailboxId, int totalEmailUnread, StreamController> onProgressController) { return Future.sync(() async { return await _mailboxIsolateWorker.markAsMailboxRead( + session, accountId, mailboxId, totalEmailUnread, onProgressController); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future subscribeMailbox(AccountId accountId, SubscribeMailboxRequest request) { + Future subscribeMailbox(Session session, AccountId accountId, SubscribeMailboxRequest request) { return Future.sync(() async { - return await mailboxAPI.subscribeMailbox(accountId, request); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await mailboxAPI.subscribeMailbox(session, accountId, request); + }).catchError(_exceptionThrower.throwException); } @override - Future> subscribeMultipleMailbox(AccountId accountId, SubscribeMultipleMailboxRequest subscribeRequest) { + Future> subscribeMultipleMailbox( + Session session, + AccountId accountId, + SubscribeMultipleMailboxRequest subscribeRequest + ) { return Future.sync(() async { - return await mailboxAPI.subscribeMultipleMailbox(accountId, subscribeRequest); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await mailboxAPI.subscribeMultipleMailbox(session, accountId, subscribeRequest); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/mailbox/data/datasource_impl/state_datasource_impl.dart b/lib/features/mailbox/data/datasource_impl/state_datasource_impl.dart index 8a496a5695..abc7dc2a99 100644 --- a/lib/features/mailbox/data/datasource_impl/state_datasource_impl.dart +++ b/lib/features/mailbox/data/datasource_impl/state_datasource_impl.dart @@ -1,6 +1,10 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; -import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/state_cache.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart'; @@ -15,26 +19,24 @@ class StateDataSourceImpl extends StateDataSource { StateDataSourceImpl(this._stateCacheClient, this._exceptionThrower); @override - Future getState(StateType stateType) { + Future getState(AccountId accountId, UserName userName, StateType stateType) { return Future.sync(() async { - final stateCache = await _stateCacheClient.getItem(stateType.value); + final stateKey = TupleKey(stateType.name, accountId.asString, userName.value).encodeKey; + final stateCache = await _stateCacheClient.getItem(stateKey); return stateCache?.toState(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future saveState(StateCache stateCache) { + Future saveState(AccountId accountId, UserName userName, StateCache stateCache) { return Future.sync(() async { final stateCacheExist = await _stateCacheClient.isExistTable(); + final stateKey = TupleKey(stateCache.type.name, accountId.asString, userName.value).encodeKey; if (stateCacheExist) { - return await _stateCacheClient.updateItem(stateCache.type.value, stateCache); + return await _stateCacheClient.updateItem(stateKey, stateCache); } else { - return await _stateCacheClient.insertItem(stateCache.type.value, stateCache); + return await _stateCacheClient.insertItem(stateKey, stateCache); } - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/mailbox/data/extensions/list_mailbox_cache_extension.dart b/lib/features/mailbox/data/extensions/list_mailbox_cache_extension.dart index de46791e78..e50c3d3330 100644 --- a/lib/features/mailbox/data/extensions/list_mailbox_cache_extension.dart +++ b/lib/features/mailbox/data/extensions/list_mailbox_cache_extension.dart @@ -1,8 +1,8 @@ +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox/data/extensions/mailbox_cache_extension.dart'; import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_cache.dart'; extension ListMailboxCacheExtension on List { - Map toMap() { - return { for (var mailboxCache in this) mailboxCache.id : mailboxCache }; - } + List toMailboxList() => map((mailboxCache) => mailboxCache.toMailbox()).toList(); } \ No newline at end of file diff --git a/lib/features/mailbox/data/extensions/list_mailbox_extension.dart b/lib/features/mailbox/data/extensions/list_mailbox_extension.dart new file mode 100644 index 0000000000..5ddabf3b4c --- /dev/null +++ b/lib/features/mailbox/data/extensions/list_mailbox_extension.dart @@ -0,0 +1,18 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:model/extensions/mailbox_id_extension.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; +import 'package:tmail_ui_user/features/mailbox/data/extensions/mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_cache.dart'; + +extension ListMailboxExtension on List { + Map toMapCache(AccountId accountId, UserName userName) { + return { + for (var mailbox in this) + TupleKey(mailbox.id!.asString, accountId.asString, userName.value).encodeKey : mailbox.toMailboxCache() + }; + } +} \ No newline at end of file diff --git a/lib/features/mailbox/data/extensions/list_mailbox_id_extension.dart b/lib/features/mailbox/data/extensions/list_mailbox_id_extension.dart new file mode 100644 index 0000000000..4f3e083c12 --- /dev/null +++ b/lib/features/mailbox/data/extensions/list_mailbox_id_extension.dart @@ -0,0 +1,12 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:model/extensions/mailbox_id_extension.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; + +extension ListMailboxIdExtension on List { + List toCacheKeyList(AccountId accountId, UserName userName) => + map((id) => TupleKey(id.asString, accountId.asString, userName.value).encodeKey).toList(); +} \ No newline at end of file diff --git a/lib/features/mailbox/data/local/mailbox_cache_manager.dart b/lib/features/mailbox/data/local/mailbox_cache_manager.dart index 9d3c3578b3..f41c87e5a2 100644 --- a/lib/features/mailbox/data/local/mailbox_cache_manager.dart +++ b/lib/features/mailbox/data/local/mailbox_cache_manager.dart @@ -1,10 +1,14 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:tmail_ui_user/features/caching/mailbox_cache_client.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/caching/clients/mailbox_cache_client.dart'; import 'package:tmail_ui_user/features/mailbox/data/extensions/list_mailbox_cache_extension.dart'; -import 'package:tmail_ui_user/features/mailbox/data/extensions/mailbox_cache_extension.dart'; -import 'package:tmail_ui_user/features/mailbox/data/extensions/mailbox_extension.dart'; -import 'package:tmail_ui_user/features/mailbox/data/model/mailbox_cache.dart'; +import 'package:tmail_ui_user/features/mailbox/data/extensions/list_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/data/extensions/list_mailbox_id_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart'; class MailboxCacheManager { @@ -12,30 +16,47 @@ class MailboxCacheManager { MailboxCacheManager(this._mailboxCacheClient); - Future> getAllMailbox() async { - final mailboxCacheList = await _mailboxCacheClient.getAll(); - final mailboxList = mailboxCacheList.map((mailboxCache) => mailboxCache.toMailbox()).toList(); - return mailboxList; + Future> getAllMailbox(AccountId accountId, UserName userName) async { + final mailboxCacheList = await _mailboxCacheClient.getListByTupleKey(accountId.asString, userName.value); + return mailboxCacheList.toMailboxList(); } - Future update({List? updated, List? created, List? destroyed}) async { + Future update( + AccountId accountId, + UserName userName, { + List? updated, + List? created, + List? destroyed + }) async { final mailboxCacheExist = await _mailboxCacheClient.isExistTable(); if (mailboxCacheExist) { - final updatedCacheMailboxes = updated - ?.map((mailbox) => mailbox.toMailboxCache()).toList() ?? []; - final createdCacheMailboxes = created - ?.map((mailbox) => mailbox.toMailboxCache()).toList() ?? []; - final destroyedCacheMailboxes = destroyed - ?.map((mailboxId) => mailboxId.id.value).toList() ?? []; + final updatedCacheMailboxes = updated?.toMapCache(accountId, userName) ?? {}; + final createdCacheMailboxes = created?.toMapCache(accountId, userName) ?? {}; + final destroyedCacheMailboxes = destroyed?.toCacheKeyList(accountId, userName) ?? []; + await Future.wait([ - _mailboxCacheClient.updateMultipleItem(updatedCacheMailboxes.toMap()), - _mailboxCacheClient.insertMultipleItem(createdCacheMailboxes.toMap()), + _mailboxCacheClient.updateMultipleItem(updatedCacheMailboxes), + _mailboxCacheClient.insertMultipleItem(createdCacheMailboxes), _mailboxCacheClient.deleteMultipleItem(destroyedCacheMailboxes) ]); } else { - final createdCacheMailboxes = created - ?.map((mailbox) => mailbox.toMailboxCache()).toList() ?? []; - await _mailboxCacheClient.insertMultipleItem(createdCacheMailboxes.toMap()); + final createdCacheMailboxes = created?.toMapCache(accountId, userName) ?? {}; + await _mailboxCacheClient.insertMultipleItem(createdCacheMailboxes); + } + return Future.value(); + } + + Future getSpamMailbox(AccountId accountId, UserName userName) async { + final mailboxCachedList = await _mailboxCacheClient.getListByTupleKey(accountId.asString, userName.value); + final listSpamMailboxCached = mailboxCachedList + .toMailboxList() + .where((mailbox) => mailbox.role == PresentationMailbox.roleSpam) + .toList(); + + if (listSpamMailboxCached.isNotEmpty) { + return listSpamMailboxCached.first; + } else { + throw NotFoundSpamMailboxCachedException(); } } } \ No newline at end of file diff --git a/lib/features/mailbox/data/model/mailbox_mark_as_read_arguments.dart b/lib/features/mailbox/data/model/mailbox_mark_as_read_arguments.dart index ade081aafc..283d3f2cfa 100644 --- a/lib/features/mailbox/data/model/mailbox_mark_as_read_arguments.dart +++ b/lib/features/mailbox/data/model/mailbox_mark_as_read_arguments.dart @@ -1,23 +1,32 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; class MailboxMarkAsReadArguments with EquatableMixin { + final Session session; final AccountId accountId; final MailboxId mailboxId; final ThreadAPI threadAPI; final EmailAPI emailAPI; MailboxMarkAsReadArguments( + this.session, this.threadAPI, this.emailAPI, this.accountId, this.mailboxId); @override - List get props => [accountId, mailboxId]; + List get props => [ + session, + accountId, + threadAPI, + emailAPI, + mailboxId + ]; } \ No newline at end of file diff --git a/lib/features/mailbox/data/model/state_type.dart b/lib/features/mailbox/data/model/state_type.dart index 852a2a9e9f..b7786c0d22 100644 --- a/lib/features/mailbox/data/model/state_type.dart +++ b/lib/features/mailbox/data/model/state_type.dart @@ -1,5 +1,9 @@ import 'package:hive/hive.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; part 'state_type.g.dart'; @@ -11,16 +15,9 @@ enum StateType { mailbox, @HiveField(1) - email -} + email; -extension StateTypeExtension on StateType { - String get value { - switch(this) { - case StateType.mailbox: - return 'mailbox'; - case StateType.email: - return 'email'; - } + String getTupleKeyStored(AccountId accountId, UserName userName) { + return TupleKey(name, accountId.asString, userName.value).encodeKey; } } \ No newline at end of file diff --git a/lib/features/mailbox/data/network/mailbox_api.dart b/lib/features/mailbox/data/network/mailbox_api.dart index cd4a88f0b7..d2ad0d757e 100644 --- a/lib/features/mailbox/data/network/mailbox_api.dart +++ b/lib/features/mailbox/data/network/mailbox_api.dart @@ -52,7 +52,8 @@ class MailboxAPI with HandleSetErrorMixin { final queryInvocation = jmapRequestBuilder.invocation(getMailboxCreated); - final capabilities = _capabilitiesForMailboxMethod(session, accountId); + final capabilities = getMailboxCreated.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); final result = await (jmapRequestBuilder ..usings(capabilities)) @@ -66,19 +67,6 @@ class MailboxAPI with HandleSetErrorMixin { return MailboxResponse(mailboxes: resultCreated?.list, state: resultCreated?.state); } - Set _capabilitiesForMailboxMethod(Session session, AccountId accountId) { - final getMailboxCreated = GetMailboxMethod(accountId); - try { - requireCapability( - session, - accountId, - [CapabilityIdentifier.jmapTeamMailboxes]); - return getMailboxCreated.requiredCapabilitiesSupportTeamMailboxes; - } catch (_) { - return getMailboxCreated.requiredCapabilities; - } - } - Future getChanges(Session session, AccountId accountId, State sinceState) async { final processingInvocation = ProcessingInvocation(); @@ -104,7 +92,8 @@ class MailboxAPI with HandleSetErrorMixin { final getMailboxUpdatedInvocation = jmapRequestBuilder.invocation(getMailboxUpdated); final getMailboxCreatedInvocation = jmapRequestBuilder.invocation(getMailboxCreated); - final capabilities = _capabilitiesForMailboxMethod(session, accountId); + final capabilities = getMailboxUpdated.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); final result = await (jmapRequestBuilder ..usings(capabilities)) @@ -135,16 +124,26 @@ class MailboxAPI with HandleSetErrorMixin { updatedProperties: resultChanges?.updatedProperties); } - Future createNewMailbox(AccountId accountId, CreateNewMailboxRequest request) async { + Future createNewMailbox(Session session, AccountId accountId, CreateNewMailboxRequest request) async { final setMailboxMethod = SetMailboxMethod(accountId) - ..addCreate(request.creationId, Mailbox(name: request.newName, parentId: request.parentId)); + ..addCreate( + request.creationId, + Mailbox( + name: request.newName, + isSubscribed: IsSubscribed(request.isSubscribed), + parentId: request.parentId + ) + ); final requestBuilder = JmapRequestBuilder(httpClient, ProcessingInvocation()); final setMailboxInvocation = requestBuilder.invocation(setMailboxMethod); + final capabilities = setMailboxMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setMailboxMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -183,11 +182,10 @@ class MailboxAPI with HandleSetErrorMixin { } Future> deleteMultipleMailbox(Session session, AccountId accountId, List mailboxIds) async { - requireCapability(session, accountId, [CapabilityIdentifier.jmapCore, CapabilityIdentifier.jmapMail]); final coreCapability = session.getCapabilityProperties( accountId, CapabilityIdentifier.jmapCore); - final maxMethodCount = coreCapability.maxCallsInRequest.value.toInt(); + final maxMethodCount = coreCapability.maxCallsInRequest?.value.toInt() ?? 0; final Map finalDeletedMailboxErrors = {}; var start = 0; @@ -211,8 +209,11 @@ class MailboxAPI with HandleSetErrorMixin { .map(requestBuilder.invocation) .toList(); + final capabilities = {CapabilityIdentifier.jmapCore, CapabilityIdentifier.jmapMail} + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings({CapabilityIdentifier.jmapCore, CapabilityIdentifier.jmapMail})) + ..usings(capabilities)) .build() .execute(); @@ -245,7 +246,7 @@ class MailboxAPI with HandleSetErrorMixin { return remainedErrors; } - Future renameMailbox(AccountId accountId, RenameMailboxRequest request) async { + Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request) async { final setMailboxMethod = SetMailboxMethod(accountId) ..addUpdates({request.mailboxId.id : PatchObject({'name' : request.newName.name})}); @@ -253,8 +254,11 @@ class MailboxAPI with HandleSetErrorMixin { final setMailboxInvocation = requestBuilder.invocation(setMailboxMethod); + final capabilities = setMailboxMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setMailboxMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -269,7 +273,7 @@ class MailboxAPI with HandleSetErrorMixin { }); } - Future moveMailbox(AccountId accountId, MoveMailboxRequest request) async { + Future moveMailbox(Session session, AccountId accountId, MoveMailboxRequest request) async { final setMailboxMethod = SetMailboxMethod(accountId) ..addUpdates({ request.mailboxId.id : PatchObject({ @@ -281,8 +285,11 @@ class MailboxAPI with HandleSetErrorMixin { final setMailboxInvocation = requestBuilder.invocation(setMailboxMethod); + final capabilities = setMailboxMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setMailboxMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -297,7 +304,7 @@ class MailboxAPI with HandleSetErrorMixin { }); } - Future subscribeMailbox(AccountId accountId, SubscribeMailboxRequest request) async { + Future subscribeMailbox(Session session, AccountId accountId, SubscribeMailboxRequest request) async { final setMailboxMethod = SetMailboxMethod(accountId) ..addUpdates({ request.mailboxId.id : PatchObject({ @@ -309,8 +316,11 @@ class MailboxAPI with HandleSetErrorMixin { final setMailboxInvocation = requestBuilder.invocation(setMailboxMethod); + final capabilities = setMailboxMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setMailboxMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -325,7 +335,11 @@ class MailboxAPI with HandleSetErrorMixin { }); } - Future> subscribeMultipleMailbox(AccountId accountId, SubscribeMultipleMailboxRequest subscribeRequest) async { + Future> subscribeMultipleMailbox( + Session session, + AccountId accountId, + SubscribeMultipleMailboxRequest subscribeRequest + ) async { final mapMailboxUpdated = subscribeRequest.mailboxIdsSubscribe .generateMapUpdateObjectSubscribeMailbox(subscribeRequest.subscribeState); @@ -336,8 +350,11 @@ class MailboxAPI with HandleSetErrorMixin { final setMailboxInvocation = requestBuilder.invocation(setMailboxMethod); + final capabilities = setMailboxMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final response = await (requestBuilder - ..usings(setMailboxMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); diff --git a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart index d0f46ae50d..4c35ae9c7d 100644 --- a/lib/features/mailbox/data/network/mailbox_isolate_worker.dart +++ b/lib/features/mailbox/data/network/mailbox_isolate_worker.dart @@ -3,10 +3,11 @@ import 'dart:async'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; @@ -34,32 +35,35 @@ class MailboxIsolateWorker { MailboxIsolateWorker(this._threadApi, this._emailApi, this._isolateExecutor); Future> markAsMailboxRead( - AccountId accountId, - MailboxId mailboxId, - int totalEmailUnread, - StreamController> onProgressController + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailUnread, + StreamController> onProgressController ) async { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return _handleMarkAsMailboxReadActionOnWeb( - accountId, - mailboxId, - totalEmailUnread, - onProgressController); + session, + accountId, + mailboxId, + totalEmailUnread, + onProgressController); } else { final result = await _isolateExecutor.execute( arg1: MailboxMarkAsReadArguments( - _threadApi, - _emailApi, - accountId, - mailboxId), + session, + _threadApi, + _emailApi, + accountId, + mailboxId), fun1: _handleMarkAsMailboxReadAction, notification: (value) { if (value is List) { log('MailboxIsolateWorker::markAsMailboxRead(): onUpdateProgress: PERCENT ${value.length / totalEmailUnread}'); onProgressController.add(Right(UpdatingMarkAsMailboxReadState( - mailboxId: mailboxId, - totalUnread: totalEmailUnread, - countRead: value.length))); + mailboxId: mailboxId, + totalUnread: totalEmailUnread, + countRead: value.length))); } }); return result; @@ -78,16 +82,18 @@ class MailboxIsolateWorker { while (mailboxHasEmails) { final emailResponse = await args.threadAPI - .getAllEmail(args.accountId, - limit: UnsignedInt(30), - filter: EmailFilterCondition( + .getAllEmail( + args.session, + args.accountId, + limit: UnsignedInt(30), + filter: EmailFilterCondition( inMailbox: args.mailboxId, notKeyword: KeyWordIdentifier.emailSeen.value, before: lastReceivedDate), - sort: {}..add( - EmailComparator(EmailComparatorProperty.receivedAt) - ..setIsAscending(false)), - properties: Properties({ + sort: {}..add( + EmailComparator(EmailComparatorProperty.receivedAt) + ..setIsAscending(false)), + properties: Properties({ EmailProperty.id, EmailProperty.keywords, EmailProperty.receivedAt, @@ -96,8 +102,8 @@ class MailboxIsolateWorker { var listEmails = response.emailList; if (listEmails != null && listEmails.isNotEmpty && lastEmailId != null) { listEmails = listEmails - .where((email) => email.id != lastEmailId) - .toList(); + .where((email) => email.id != lastEmailId) + .toList(); } return EmailsResponse(emailList: listEmails, state: response.state); }); @@ -112,9 +118,10 @@ class MailboxIsolateWorker { lastReceivedDate = listEmailUnread.last.receivedAt; final result = await args.emailAPI.markAsRead( - args.accountId, - listEmailUnread, - ReadActions.markAsRead); + args.session, + args.accountId, + listEmailUnread, + ReadActions.markAsRead); log('MailboxIsolateWorker::_handleMarkAsMailboxRead(): MARK_READ: ${result.length}'); emailListCompleted.addAll(result); @@ -129,10 +136,11 @@ class MailboxIsolateWorker { } Future> _handleMarkAsMailboxReadActionOnWeb( - AccountId accountId, - MailboxId mailboxId, - int totalEmailUnread, - StreamController> onProgressController + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailUnread, + StreamController> onProgressController ) async { List emailListCompleted = List.empty(growable: true); try { @@ -142,28 +150,30 @@ class MailboxIsolateWorker { while (mailboxHasEmails) { final emailResponse = await _threadApi - .getAllEmail(accountId, - limit: UnsignedInt(30), - filter: EmailFilterCondition( - inMailbox: mailboxId, - notKeyword: KeyWordIdentifier.emailSeen.value, - before: lastReceivedDate), - sort: {}..add( + .getAllEmail( + session, + accountId, + limit: UnsignedInt(30), + filter: EmailFilterCondition( + inMailbox: mailboxId, + notKeyword: KeyWordIdentifier.emailSeen.value, + before: lastReceivedDate), + sort: {}..add( EmailComparator(EmailComparatorProperty.receivedAt) ..setIsAscending(false)), - properties: Properties({ + properties: Properties({ EmailProperty.id, EmailProperty.keywords, EmailProperty.receivedAt, - })) - .then((response) { - var listEmails = response.emailList; - if (listEmails != null && listEmails.isNotEmpty && lastEmailId != null) { - listEmails = listEmails - .where((email) => email.id != lastEmailId) - .toList(); - } - return EmailsResponse(emailList: listEmails, state: response.state); + }) + ).then((response) { + var listEmails = response.emailList; + if (listEmails != null && listEmails.isNotEmpty && lastEmailId != null) { + listEmails = listEmails + .where((email) => email.id != lastEmailId) + .toList(); + } + return EmailsResponse(emailList: listEmails, state: response.state); }); final listEmailUnread = emailResponse.emailList; @@ -175,8 +185,7 @@ class MailboxIsolateWorker { lastEmailId = listEmailUnread.last.id; lastReceivedDate = listEmailUnread.last.receivedAt; - final result = await _emailApi.markAsRead( - accountId, listEmailUnread, ReadActions.markAsRead); + final result = await _emailApi.markAsRead(session, accountId, listEmailUnread, ReadActions.markAsRead); log('MailboxIsolateWorker::_handleMarkAsMailboxReadActionOnWeb(): MARK_READ: ${result.length}'); emailListCompleted.addAll(result); diff --git a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart index 885472c2d9..4a087eb1af 100644 --- a/lib/features/mailbox/data/repository/mailbox_repository_impl.dart +++ b/lib/features/mailbox/data/repository/mailbox_repository_impl.dart @@ -39,8 +39,8 @@ class MailboxRepositoryImpl extends MailboxRepository { @override Stream getAllMailbox(Session session, AccountId accountId, {Properties? properties}) async* { final localMailboxResponse = await Future.wait([ - mapDataSource[DataSourceType.local]!.getAllMailboxCache(), - stateDataSource.getState(StateType.mailbox) + mapDataSource[DataSourceType.local]!.getAllMailboxCache(accountId, session.username), + stateDataSource.getState(accountId, session.username, StateType.mailbox) ]).then((List response) { return MailboxResponse(mailboxes: response.first, state: response.last); }); @@ -64,26 +64,28 @@ class MailboxRepositoryImpl extends MailboxRepository { await Future.wait([ mapDataSource[DataSourceType.local]!.update( + accountId, + session.username, updated: newMailboxUpdated, created: changesResponse.created, destroyed: changesResponse.destroyed), if (changesResponse.newStateMailbox != null) - stateDataSource.saveState(changesResponse.newStateMailbox!.toStateCache(StateType.mailbox)), + stateDataSource.saveState(accountId, session.username, changesResponse.newStateMailbox!.toStateCache(StateType.mailbox)), ]); } } else { final mailboxResponse = await mapDataSource[DataSourceType.network]!.getAllMailbox(session, accountId); await Future.wait([ - mapDataSource[DataSourceType.local]!.update(created: mailboxResponse.mailboxes), + mapDataSource[DataSourceType.local]!.update(accountId, session.username, created: mailboxResponse.mailboxes), if (mailboxResponse.state != null) - stateDataSource.saveState(mailboxResponse.state!.toStateCache(StateType.mailbox)), + stateDataSource.saveState(accountId, session.username, mailboxResponse.state!.toStateCache(StateType.mailbox)), ]); } final newMailboxResponse = await Future.wait([ - mapDataSource[DataSourceType.local]!.getAllMailboxCache(), - stateDataSource.getState(StateType.mailbox) + mapDataSource[DataSourceType.local]!.getAllMailboxCache(accountId, session.username), + stateDataSource.getState(accountId, session.username, StateType.mailbox) ]).then((List response) { return MailboxResponse(mailboxes: response.first, state: response.last); }); @@ -117,7 +119,7 @@ class MailboxRepositoryImpl extends MailboxRepository { @override Stream refresh(Session session, AccountId accountId, State currentState) async* { - final localMailboxList = await mapDataSource[DataSourceType.local]!.getAllMailboxCache(); + final localMailboxList = await mapDataSource[DataSourceType.local]!.getAllMailboxCache(accountId, session.username); bool hasMoreChanges = true; State? sinceState = currentState; @@ -135,17 +137,19 @@ class MailboxRepositoryImpl extends MailboxRepository { await Future.wait([ mapDataSource[DataSourceType.local]!.update( + accountId, + session.username, updated: newMailboxUpdated, created: changesResponse.created, destroyed: changesResponse.destroyed), if (changesResponse.newStateMailbox != null) - stateDataSource.saveState(changesResponse.newStateMailbox!.toStateCache(StateType.mailbox)), + stateDataSource.saveState(accountId, session.username, changesResponse.newStateMailbox!.toStateCache(StateType.mailbox)), ]); } final newMailboxResponse = await Future.wait([ - mapDataSource[DataSourceType.local]!.getAllMailboxCache(), - stateDataSource.getState(StateType.mailbox) + mapDataSource[DataSourceType.local]!.getAllMailboxCache(accountId, session.username), + stateDataSource.getState(accountId, session.username, StateType.mailbox) ]).then((List response) { return MailboxResponse(mailboxes: response.first, state: response.last); }); @@ -154,8 +158,8 @@ class MailboxRepositoryImpl extends MailboxRepository { } @override - Future createNewMailbox(AccountId accountId, CreateNewMailboxRequest newMailboxRequest) { - return mapDataSource[DataSourceType.network]!.createNewMailbox(accountId, newMailboxRequest); + Future createNewMailbox(Session session, AccountId accountId, CreateNewMailboxRequest newMailboxRequest) { + return mapDataSource[DataSourceType.network]!.createNewMailbox(session, accountId, newMailboxRequest); } @override @@ -164,17 +168,19 @@ class MailboxRepositoryImpl extends MailboxRepository { } @override - Future renameMailbox(AccountId accountId, RenameMailboxRequest request) { - return mapDataSource[DataSourceType.network]!.renameMailbox(accountId, request); + Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request) { + return mapDataSource[DataSourceType.network]!.renameMailbox(session, accountId, request); } @override Future> markAsMailboxRead( + Session session, AccountId accountId, MailboxId mailboxId, int totalEmailUnread, StreamController> onProgressController) async { return mapDataSource[DataSourceType.network]!.markAsMailboxRead( + session, accountId, mailboxId, totalEmailUnread, @@ -182,22 +188,22 @@ class MailboxRepositoryImpl extends MailboxRepository { } @override - Future moveMailbox(AccountId accountId, MoveMailboxRequest request) { - return mapDataSource[DataSourceType.network]!.moveMailbox(accountId, request); + Future moveMailbox(Session session, AccountId accountId, MoveMailboxRequest request) { + return mapDataSource[DataSourceType.network]!.moveMailbox(session, accountId, request); } @override - Future getMailboxState() { - return stateDataSource.getState(StateType.mailbox); + Future getMailboxState(Session session, AccountId accountId) { + return stateDataSource.getState(accountId, session.username, StateType.mailbox); } @override - Future subscribeMailbox(AccountId accountId, SubscribeMailboxRequest request) { - return mapDataSource[DataSourceType.network]!.subscribeMailbox(accountId, request); + Future subscribeMailbox(Session session, AccountId accountId, SubscribeMailboxRequest request) { + return mapDataSource[DataSourceType.network]!.subscribeMailbox(session, accountId, request); } @override - Future> subscribeMultipleMailbox(AccountId accountId, SubscribeMultipleMailboxRequest subscribeRequest) { - return mapDataSource[DataSourceType.network]!.subscribeMultipleMailbox(accountId, subscribeRequest); + Future> subscribeMultipleMailbox(Session session, AccountId accountId, SubscribeMultipleMailboxRequest subscribeRequest) { + return mapDataSource[DataSourceType.network]!.subscribeMultipleMailbox(session, accountId, subscribeRequest); } } \ No newline at end of file diff --git a/lib/features/mailbox/domain/extensions/presentation_mailbox_extension.dart b/lib/features/mailbox/domain/extensions/presentation_mailbox_extension.dart deleted file mode 100644 index 2d80ed044b..0000000000 --- a/lib/features/mailbox/domain/extensions/presentation_mailbox_extension.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:core/core.dart'; -import 'package:model/model.dart'; - -extension PresentationMailboxExtension on PresentationMailbox { - - String getMailboxIcon(ImagePaths imagePaths) { - if (hasRole()) { - switch(role!.value) { - case 'inbox': - return imagePaths.icMailboxInbox; - case 'drafts': - return imagePaths.icMailboxDrafts; - case 'archive': - return imagePaths.icMailboxArchived; - case 'sent': - return imagePaths.icMailboxSent; - case 'trash': - return imagePaths.icMailboxTrash; - case 'spam': - return imagePaths.icMailboxSpam; - case 'templates': - return imagePaths.icMailboxTemplate; - case 'all_mail': - return imagePaths.icMailboxAllMail; - default: - return imagePaths.icFolderMailbox; - } - } else if (isChildOfTeamMailboxes) { - switch(name!.name.toLowerCase()) { - case 'inbox': - return imagePaths.icMailboxInbox; - case 'drafts': - return imagePaths.icMailboxDrafts; - case 'archive': - return imagePaths.icMailboxArchived; - case 'sent': - return imagePaths.icMailboxSent; - case 'trash': - return imagePaths.icMailboxTrash; - case 'spam': - return imagePaths.icMailboxSpam; - case 'templates': - return imagePaths.icMailboxTemplate; - default: - return imagePaths.icFolderMailbox; - } - } else { - return imagePaths.icFolderMailbox; - } - } -} \ No newline at end of file diff --git a/lib/features/mailbox/domain/model/create_new_mailbox_request.dart b/lib/features/mailbox/domain/model/create_new_mailbox_request.dart index 280f232c9e..3c50807a5f 100644 --- a/lib/features/mailbox/domain/model/create_new_mailbox_request.dart +++ b/lib/features/mailbox/domain/model/create_new_mailbox_request.dart @@ -8,9 +8,22 @@ class CreateNewMailboxRequest with EquatableMixin { final MailboxName newName; final Id creationId; final MailboxId? parentId; + final bool isSubscribed; - CreateNewMailboxRequest(this.creationId, this.newName, {this.parentId}); + CreateNewMailboxRequest( + this.creationId, + this.newName, + { + this.parentId, + this.isSubscribed = true + } + ); @override - List get props => [creationId, newName, parentId]; + List get props => [ + creationId, + newName, + parentId, + isSubscribed + ]; } \ No newline at end of file diff --git a/lib/features/mailbox/domain/model/move_mailbox_request.dart b/lib/features/mailbox/domain/model/move_mailbox_request.dart index 8b1f0130b2..31c845e60a 100644 --- a/lib/features/mailbox/domain/model/move_mailbox_request.dart +++ b/lib/features/mailbox/domain/model/move_mailbox_request.dart @@ -6,7 +6,7 @@ import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; class MoveMailboxRequest with EquatableMixin { final MailboxId? destinationMailboxId; - final MailboxName? destinationMailboxName; + final String? destinationMailboxDisplayName; final MailboxId? parentId; final MailboxId mailboxId; final MoveAction moveAction; @@ -17,7 +17,7 @@ class MoveMailboxRequest with EquatableMixin { { this.parentId, this.destinationMailboxId, - this.destinationMailboxName + this.destinationMailboxDisplayName } ); @@ -27,6 +27,6 @@ class MoveMailboxRequest with EquatableMixin { moveAction, parentId, destinationMailboxId, - destinationMailboxName + destinationMailboxDisplayName ]; } \ No newline at end of file diff --git a/lib/features/mailbox/domain/repository/mailbox_repository.dart b/lib/features/mailbox/domain/repository/mailbox_repository.dart index 71aa228aff..efcc5e332f 100644 --- a/lib/features/mailbox/domain/repository/mailbox_repository.dart +++ b/lib/features/mailbox/domain/repository/mailbox_repository.dart @@ -23,23 +23,24 @@ abstract class MailboxRepository { Stream refresh(Session session, AccountId accountId, State currentState); - Future createNewMailbox(AccountId accountId, CreateNewMailboxRequest newMailboxRequest); + Future createNewMailbox(Session session, AccountId accountId, CreateNewMailboxRequest newMailboxRequest); Future> deleteMultipleMailbox(Session session, AccountId accountId, List mailboxIds); - Future renameMailbox(AccountId accountId, RenameMailboxRequest request); + Future renameMailbox(Session session, AccountId accountId, RenameMailboxRequest request); Future> markAsMailboxRead( - AccountId accountId, - MailboxId mailboxId, - int totalEmailUnread, - StreamController> onProgressController); + Session session, + AccountId accountId, + MailboxId mailboxId, + int totalEmailUnread, + StreamController> onProgressController); - Future moveMailbox(AccountId accountId, MoveMailboxRequest request); + Future moveMailbox(Session session, AccountId accountId, MoveMailboxRequest request); - Future getMailboxState(); + Future getMailboxState(Session session, AccountId accountId); - Future subscribeMailbox(AccountId accountId, SubscribeMailboxRequest request); + Future subscribeMailbox(Session session, AccountId accountId, SubscribeMailboxRequest request); - Future> subscribeMultipleMailbox(AccountId accountId, SubscribeMultipleMailboxRequest subscribeRequest); + Future> subscribeMultipleMailbox(Session session, AccountId accountId, SubscribeMultipleMailboxRequest subscribeRequest); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/state/create_new_mailbox_state.dart b/lib/features/mailbox/domain/state/create_new_mailbox_state.dart index e428286872..ada9c183ff 100644 --- a/lib/features/mailbox/domain/state/create_new_mailbox_state.dart +++ b/lib/features/mailbox/domain/state/create_new_mailbox_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -15,14 +16,10 @@ class CreateNewMailboxSuccess extends UIActionState { }) : super(currentEmailState, currentMailboxState); @override - List get props => [newMailbox]; + List get props => [newMailbox, ...super.props]; } class CreateNewMailboxFailure extends FeatureFailure { - final dynamic exception; - CreateNewMailboxFailure(this.exception); - - @override - List get props => [exception]; + CreateNewMailboxFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/state/delete_multiple_mailbox_state.dart b/lib/features/mailbox/domain/state/delete_multiple_mailbox_state.dart index 33627d209e..dc4ce30f22 100644 --- a/lib/features/mailbox/domain/state/delete_multiple_mailbox_state.dart +++ b/lib/features/mailbox/domain/state/delete_multiple_mailbox_state.dart @@ -16,7 +16,7 @@ class DeleteMultipleMailboxAllSuccess extends UIActionState { }) : super(currentEmailState, currentMailboxState); @override - List get props => [listMailboxIdDeleted]; + List get props => [listMailboxIdDeleted, ...super.props]; } class DeleteMultipleMailboxHasSomeSuccess extends UIActionState { @@ -29,22 +29,12 @@ class DeleteMultipleMailboxHasSomeSuccess extends UIActionState { }) : super(currentEmailState, currentMailboxState); @override - List get props => [listMailboxIdDeleted]; + List get props => [listMailboxIdDeleted, ...super.props]; } -class DeleteMultipleMailboxAllFailure extends FeatureFailure { - - DeleteMultipleMailboxAllFailure(); - - @override - List get props => []; -} +class DeleteMultipleMailboxAllFailure extends FeatureFailure {} class DeleteMultipleMailboxFailure extends FeatureFailure { - final dynamic exception; - - DeleteMultipleMailboxFailure(this.exception); - @override - List get props => [exception]; + DeleteMultipleMailboxFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/state/get_all_mailboxes_state.dart b/lib/features/mailbox/domain/state/get_all_mailboxes_state.dart index 1c5d4ac02c..3cb2486a6a 100644 --- a/lib/features/mailbox/domain/state/get_all_mailboxes_state.dart +++ b/lib/features/mailbox/domain/state/get_all_mailboxes_state.dart @@ -2,6 +2,8 @@ import 'package:core/core.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; +class GetAllMailboxLoading extends LoadingState {} + class GetAllMailboxSuccess extends UIState { final List mailboxList; final State? currentMailboxState; @@ -16,10 +18,6 @@ class GetAllMailboxSuccess extends UIState { } class GetAllMailboxFailure extends FeatureFailure { - final dynamic exception; - - GetAllMailboxFailure(this.exception); - @override - List get props => [exception]; + GetAllMailboxFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart index 7f2b484cbf..19ddb82993 100644 --- a/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart +++ b/lib/features/mailbox/domain/state/mark_as_mailbox_read_state.dart @@ -1,15 +1,10 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; -class MarkAsMailboxReadLoading extends UIState { - - MarkAsMailboxReadLoading(); - - @override - List get props => []; -} +class MarkAsMailboxReadLoading extends UIState {} class UpdatingMarkAsMailboxReadState extends UIState { @@ -28,9 +23,9 @@ class UpdatingMarkAsMailboxReadState extends UIState { class MarkAsMailboxReadAllSuccess extends UIActionState { - final MailboxName mailboxName; + final String mailboxDisplayName; - MarkAsMailboxReadAllSuccess(this.mailboxName, + MarkAsMailboxReadAllSuccess(this.mailboxDisplayName, { jmap.State? currentEmailState, jmap.State? currentMailboxState, @@ -38,16 +33,19 @@ class MarkAsMailboxReadAllSuccess extends UIActionState { ) : super(currentMailboxState, currentEmailState); @override - List get props => [mailboxName]; + List get props => [ + mailboxDisplayName, + ...super.props + ]; } class MarkAsMailboxReadHasSomeEmailFailure extends UIActionState { - final MailboxName mailboxName; + final String mailboxDisplayName; final int countEmailsRead; MarkAsMailboxReadHasSomeEmailFailure( - this.mailboxName, + this.mailboxDisplayName, this.countEmailsRead, { jmap.State? currentEmailState, @@ -56,22 +54,16 @@ class MarkAsMailboxReadHasSomeEmailFailure extends UIActionState { ) : super(currentMailboxState, currentEmailState); @override - List get props => [mailboxName, countEmailsRead]; + List get props => [ + mailboxDisplayName, + countEmailsRead, + ...super.props + ]; } -class MarkAsMailboxReadAllFailure extends FeatureFailure { - - MarkAsMailboxReadAllFailure(); - - @override - List get props => []; -} +class MarkAsMailboxReadAllFailure extends FeatureFailure {} class MarkAsMailboxReadFailure extends FeatureFailure { - final dynamic exception; - MarkAsMailboxReadFailure(this.exception); - - @override - List get props => [exception]; + MarkAsMailboxReadFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/state/move_mailbox_state.dart b/lib/features/mailbox/domain/state/move_mailbox_state.dart index 8d776f4689..1b9a8a5791 100644 --- a/lib/features/mailbox/domain/state/move_mailbox_state.dart +++ b/lib/features/mailbox/domain/state/move_mailbox_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; @@ -12,7 +13,7 @@ class MoveMailboxSuccess extends UIActionState { final MoveAction moveAction; final MailboxId? parentId; final MailboxId? destinationMailboxId; - final MailboxName? destinationMailboxName; + final String? destinationMailboxDisplayName; MoveMailboxSuccess( this.mailboxIdSelected, @@ -20,7 +21,7 @@ class MoveMailboxSuccess extends UIActionState { { this.parentId, this.destinationMailboxId, - this.destinationMailboxName, + this.destinationMailboxDisplayName, jmap.State? currentEmailState, jmap.State? currentMailboxState, } @@ -29,15 +30,15 @@ class MoveMailboxSuccess extends UIActionState { @override List get props => [ mailboxIdSelected, + moveAction, parentId, - destinationMailboxId]; + destinationMailboxId, + destinationMailboxDisplayName, + ...super.props + ]; } class MoveMailboxFailure extends FeatureFailure { - final dynamic exception; - MoveMailboxFailure(this.exception); - - @override - List get props => [exception]; + MoveMailboxFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/state/refresh_changes_all_mailboxes_state.dart b/lib/features/mailbox/domain/state/refresh_changes_all_mailboxes_state.dart index 4444d41374..2a17dde55f 100644 --- a/lib/features/mailbox/domain/state/refresh_changes_all_mailboxes_state.dart +++ b/lib/features/mailbox/domain/state/refresh_changes_all_mailboxes_state.dart @@ -1,7 +1,10 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; +class RefreshChangesAllMailboxLoading extends LoadingState {} + class RefreshChangesAllMailboxSuccess extends UIState { final List mailboxList; final State? currentMailboxState; @@ -16,10 +19,6 @@ class RefreshChangesAllMailboxSuccess extends UIState { } class RefreshChangesAllMailboxFailure extends FeatureFailure { - final dynamic exception; - - RefreshChangesAllMailboxFailure(this.exception); - @override - List get props => [exception]; + RefreshChangesAllMailboxFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/state/rename_mailbox_state.dart b/lib/features/mailbox/domain/state/rename_mailbox_state.dart index 5dba4081b0..988f8760f6 100644 --- a/lib/features/mailbox/domain/state/rename_mailbox_state.dart +++ b/lib/features/mailbox/domain/state/rename_mailbox_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -10,16 +11,9 @@ class RenameMailboxSuccess extends UIActionState { jmap.State? currentEmailState, jmap.State? currentMailboxState, }) : super(currentEmailState, currentMailboxState); - - @override - List get props => []; } class RenameMailboxFailure extends FeatureFailure { - final dynamic exception; - - RenameMailboxFailure(this.exception); - @override - List get props => [exception]; + RenameMailboxFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/state/search_mailbox_state.dart b/lib/features/mailbox/domain/state/search_mailbox_state.dart index aec50e3e5f..d7af4e09df 100644 --- a/lib/features/mailbox/domain/state/search_mailbox_state.dart +++ b/lib/features/mailbox/domain/state/search_mailbox_state.dart @@ -1,7 +1,8 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; -class LoadingSearchMailbox extends UIState {} +class LoadingSearchMailbox extends LoadingState {} class SearchMailboxSuccess extends UIState { @@ -14,10 +15,6 @@ class SearchMailboxSuccess extends UIState { } class SearchMailboxFailure extends FeatureFailure { - final dynamic exception; - SearchMailboxFailure(this.exception); - - @override - List get props => [exception]; + SearchMailboxFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/state/subscribe_mailbox_state.dart b/lib/features/mailbox/domain/state/subscribe_mailbox_state.dart index 1901736ad9..ca4503ceaf 100644 --- a/lib/features/mailbox/domain/state/subscribe_mailbox_state.dart +++ b/lib/features/mailbox/domain/state/subscribe_mailbox_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -23,15 +24,11 @@ class SubscribeMailboxSuccess extends UIActionState { List get props => [ mailboxId, subscribeAction, - super.props + ...super.props ]; } class SubscribeMailboxFailure extends FeatureFailure { - final dynamic exception; - SubscribeMailboxFailure(this.exception); - - @override - List get props => [exception]; + SubscribeMailboxFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/state/subscribe_multiple_mailbox_state.dart b/lib/features/mailbox/domain/state/subscribe_multiple_mailbox_state.dart index e8af12fbc8..2f22eff127 100644 --- a/lib/features/mailbox/domain/state/subscribe_multiple_mailbox_state.dart +++ b/lib/features/mailbox/domain/state/subscribe_multiple_mailbox_state.dart @@ -28,7 +28,7 @@ class SubscribeMultipleMailboxAllSuccess extends UIActionState { parentMailboxId, mailboxIdsSubscribe, subscribeAction, - super.props + ...super.props ]; } @@ -53,23 +53,13 @@ class SubscribeMultipleMailboxHasSomeSuccess extends UIActionState { parentMailboxId, mailboxIdsSubscribe, subscribeAction, - super.props + ...super.props ]; } -class SubscribeMultipleMailboxAllFailure extends FeatureFailure { - - SubscribeMultipleMailboxAllFailure(); - - @override - List get props => []; -} +class SubscribeMultipleMailboxAllFailure extends FeatureFailure {} class SubscribeMultipleMailboxFailure extends FeatureFailure { - final dynamic exception; - - SubscribeMultipleMailboxFailure(this.exception); - @override - List get props => [exception]; + SubscribeMultipleMailboxFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart index 43ce3d0d63..0f3f12ef0e 100644 --- a/lib/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/create_new_mailbox_state.dart'; @@ -10,12 +11,16 @@ class CreateNewMailboxInteractor { CreateNewMailboxInteractor(this._mailboxRepository); - Stream> execute(AccountId accountId, CreateNewMailboxRequest newMailboxRequest) async* { + Stream> execute( + Session session, + AccountId accountId, + CreateNewMailboxRequest newMailboxRequest + ) async* { try { yield Right(LoadingCreateNewMailbox()); - final currentMailboxState = await _mailboxRepository.getMailboxState(); - final newMailbox = await _mailboxRepository.createNewMailbox(accountId, newMailboxRequest); + final currentMailboxState = await _mailboxRepository.getMailboxState(session, accountId); + final newMailbox = await _mailboxRepository.createNewMailbox(session, accountId, newMailboxRequest); if (newMailbox != null) { yield Right(CreateNewMailboxSuccess( newMailbox, diff --git a/lib/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart index 5cad6c1724..ee09dcc6fb 100644 --- a/lib/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart @@ -22,7 +22,7 @@ class DeleteMultipleMailboxInteractor { try { yield Right(LoadingDeleteMultipleMailboxAll()); - final currentMailboxState = await _mailboxRepository.getMailboxState(); + final currentMailboxState = await _mailboxRepository.getMailboxState(session, accountId); final listResult = await Future.wait( mapMailboxIdToDelete.keys.map((mailboxId) { diff --git a/lib/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart index 3f58fbea68..9bedcc054e 100644 --- a/lib/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart @@ -15,7 +15,7 @@ class GetAllMailboxInteractor { Stream> execute(Session session, AccountId accountId, {Properties? properties}) async* { try { - yield Right(LoadingState()); + yield Right(GetAllMailboxLoading()); yield* _mailboxRepository .getAllMailbox( diff --git a/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart b/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart index eafb0b778f..b078d9061e 100644 --- a/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart +++ b/lib/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; @@ -15,41 +16,43 @@ class MarkAsMailboxReadInteractor { MarkAsMailboxReadInteractor(this._mailboxRepository, this._emailRepository); Stream> execute( - AccountId accountId, - MailboxId mailboxId, - MailboxName mailboxName, - int totalEmailUnread, - StreamController> onProgressController + Session session, + AccountId accountId, + MailboxId mailboxId, + String mailboxDisplayName, + int totalEmailUnread, + StreamController> onProgressController ) async* { try { yield Right(MarkAsMailboxReadLoading()); onProgressController.add(Right(MarkAsMailboxReadLoading())); final listState = await Future.wait([ - _mailboxRepository.getMailboxState(), - _emailRepository.getEmailState(), + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), ], eagerError: true); final currentMailboxState = listState.first; final currentEmailState = listState.last; final listEmails = await _mailboxRepository.markAsMailboxRead( - accountId, - mailboxId, - totalEmailUnread, - onProgressController); + session, + accountId, + mailboxId, + totalEmailUnread, + onProgressController); if (totalEmailUnread == listEmails.length) { yield Right(MarkAsMailboxReadAllSuccess( - mailboxName, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); + mailboxDisplayName, + currentEmailState: currentEmailState, + currentMailboxState: currentMailboxState)); } else if (listEmails.isNotEmpty) { yield Right(MarkAsMailboxReadHasSomeEmailFailure( - mailboxName, - listEmails.length, - currentEmailState: currentEmailState, - currentMailboxState: currentMailboxState)); + mailboxDisplayName, + listEmails.length, + currentEmailState: currentEmailState, + currentMailboxState: currentMailboxState)); } else { yield Left(MarkAsMailboxReadAllFailure()); } diff --git a/lib/features/mailbox/domain/usecases/move_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/move_mailbox_interactor.dart index b5da230879..acf3687af3 100644 --- a/lib/features/mailbox/domain/usecases/move_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/move_mailbox_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/move_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/move_mailbox_state.dart'; @@ -10,19 +11,19 @@ class MoveMailboxInteractor { MoveMailboxInteractor(this._mailboxRepository); - Stream> execute(AccountId accountId, MoveMailboxRequest request) async* { + Stream> execute(Session session, AccountId accountId, MoveMailboxRequest request) async* { try { yield Right(LoadingMoveMailbox()); - final currentMailboxState = await _mailboxRepository.getMailboxState(); - final result = await _mailboxRepository.moveMailbox(accountId, request); + final currentMailboxState = await _mailboxRepository.getMailboxState(session, accountId); + final result = await _mailboxRepository.moveMailbox(session, accountId, request); if (result) { yield Right(MoveMailboxSuccess( request.mailboxId, request.moveAction, parentId: request.parentId, destinationMailboxId: request.destinationMailboxId, - destinationMailboxName: request.destinationMailboxName, + destinationMailboxDisplayName: request.destinationMailboxDisplayName, currentMailboxState: currentMailboxState)); } else { yield Left(MoveMailboxFailure(null)); diff --git a/lib/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart index 4eb288474e..f8d9cb9164 100644 --- a/lib/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart @@ -1,9 +1,11 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart' as jmapState; -import 'package:model/model.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart' as jmap_state; +import 'package:model/extensions/mailbox_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_response.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/refresh_changes_all_mailboxes_state.dart'; @@ -13,10 +15,9 @@ class RefreshAllMailboxInteractor { RefreshAllMailboxInteractor(this._mailboxRepository); - Stream> execute(Session session, AccountId accountId, jmapState.State currentState) async* { + Stream> execute(Session session, AccountId accountId, jmap_state.State currentState) async* { try { - yield Right(RefreshingState()); - + yield Right(RefreshChangesAllMailboxLoading()); yield* _mailboxRepository .refresh(session, accountId, currentState) .map(_toGetMailboxState); diff --git a/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart index bd06dd70d1..e6cd5f57f4 100644 --- a/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/rename_mailbox_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/rename_mailbox_state.dart'; @@ -10,13 +11,13 @@ class RenameMailboxInteractor { RenameMailboxInteractor(this._mailboxRepository); - Stream> execute(AccountId accountId, RenameMailboxRequest request) async* { + Stream> execute(Session session, AccountId accountId, RenameMailboxRequest request) async* { try { yield Right(LoadingRenameMailbox()); - final currentMailboxState = await _mailboxRepository.getMailboxState(); + final currentMailboxState = await _mailboxRepository.getMailboxState(session, accountId); - final result = await _mailboxRepository.renameMailbox(accountId, request); + final result = await _mailboxRepository.renameMailbox(session, accountId, request); if (result) { yield Right(RenameMailboxSuccess(currentMailboxState: currentMailboxState)); } else { diff --git a/lib/features/mailbox/domain/usecases/search_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/search_mailbox_interactor.dart index 7d15066601..1cacf3c799 100644 --- a/lib/features/mailbox/domain/usecases/search_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/search_mailbox_interactor.dart @@ -12,7 +12,7 @@ class SearchMailboxInteractor { yield Right(LoadingSearchMailbox()); final resultList = mailboxes - .where((mailbox) => mailbox.name?.name.toLowerCase().contains(searchQuery.value.toLowerCase()) == true) + .where((mailbox) => _matchMailboxByQuery(mailbox, searchQuery)) .toList(); yield Right(SearchMailboxSuccess(resultList)); @@ -20,4 +20,13 @@ class SearchMailboxInteractor { yield Left(SearchMailboxFailure(exception)); } } + + bool _matchMailboxByQuery(PresentationMailbox mailbox, SearchQuery searchQuery) { + if (mailbox.displayName == null) { + return false; + } else { + return mailbox.displayName!.toLowerCase().contains(searchQuery.value.toLowerCase()) || + searchQuery.value.toLowerCase().contains(mailbox.displayName!.toLowerCase()); + } + } } \ No newline at end of file diff --git a/lib/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart index ee63f242a3..267eedb4cf 100644 --- a/lib/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart @@ -2,6 +2,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/subscribe_mailbox_state.dart'; @@ -11,13 +12,13 @@ class SubscribeMailboxInteractor { SubscribeMailboxInteractor(this._mailboxRepository); - Stream> execute(AccountId accountId, SubscribeMailboxRequest request) async* { + Stream> execute(Session session, AccountId accountId, SubscribeMailboxRequest request) async* { try { yield Right(LoadingSubscribeMailbox()); - final currentMailboxState = await _mailboxRepository.getMailboxState(); + final currentMailboxState = await _mailboxRepository.getMailboxState(session, accountId); - final result = await _mailboxRepository.subscribeMailbox(accountId, request); + final result = await _mailboxRepository.subscribeMailbox(session, accountId, request); if (result) { yield Right(SubscribeMailboxSuccess( diff --git a/lib/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart b/lib/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart index 019d526df9..520c38fff4 100644 --- a/lib/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart +++ b/lib/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart @@ -2,6 +2,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_multiple_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/subscribe_multiple_mailbox_state.dart'; @@ -11,12 +12,16 @@ class SubscribeMultipleMailboxInteractor { SubscribeMultipleMailboxInteractor(this._mailboxRepository); - Stream> execute(AccountId accountId, SubscribeMultipleMailboxRequest subscribeRequest) async* { + Stream> execute( + Session session, + AccountId accountId, + SubscribeMultipleMailboxRequest subscribeRequest + ) async* { try { yield Right(LoadingSubscribeMultipleMailbox()); - final currentMailboxState = await _mailboxRepository.getMailboxState(); - final listResult = await _mailboxRepository.subscribeMultipleMailbox(accountId, subscribeRequest); + final currentMailboxState = await _mailboxRepository.getMailboxState(session, accountId); + final listResult = await _mailboxRepository.subscribeMultipleMailbox(session, accountId, subscribeRequest); final matchedSize = listResult.length == subscribeRequest.mailboxIdsSubscribe.length; final allMatchedMailboxIdSubscribe = subscribeRequest.mailboxIdsSubscribe diff --git a/lib/features/mailbox/presentation/action/mailbox_ui_action.dart b/lib/features/mailbox/presentation/action/mailbox_ui_action.dart index 2d522840dd..2a0dd928b0 100644 --- a/lib/features/mailbox/presentation/action/mailbox_ui_action.dart +++ b/lib/features/mailbox/presentation/action/mailbox_ui_action.dart @@ -1,4 +1,5 @@ +import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -11,12 +12,7 @@ class MailboxUIAction extends UIAction { List get props => []; } -class SelectMailboxDefaultAction extends MailboxUIAction { - SelectMailboxDefaultAction(); - - @override - List get props => []; -} +class SelectMailboxDefaultAction extends MailboxUIAction {} class RefreshChangeMailboxAction extends MailboxUIAction { final jmap.State? newState; @@ -25,4 +21,14 @@ class RefreshChangeMailboxAction extends MailboxUIAction { @override List get props => [newState]; +} + +class OpenMailboxAction extends MailboxUIAction { + + final PresentationMailbox presentationMailbox; + + OpenMailboxAction(this.presentationMailbox); + + @override + List get props => [presentationMailbox]; } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/base_mailbox_view.dart b/lib/features/mailbox/presentation/base_mailbox_view.dart new file mode 100644 index 0000000000..4bb31a17a4 --- /dev/null +++ b/lib/features/mailbox/presentation/base_mailbox_view.dart @@ -0,0 +1,18 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_controller.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart'; + +abstract class BaseMailboxView extends GetWidget + with + AppLoaderMixin, + MailboxWidgetMixin { + + BaseMailboxView({Key? key}) : super(key: key); + + final responsiveUtils = Get.find(); + final imagePaths = Get.find(); +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart b/lib/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart new file mode 100644 index 0000000000..e473b3dcbc --- /dev/null +++ b/lib/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart @@ -0,0 +1,88 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/navigation_router.dart'; +import 'package:tmail_ui_user/main/routes/route_utils.dart'; + +extension PresentationMailboxExtension on PresentationMailbox { + + String getDisplayName(BuildContext context) { + if (isDefault) { + switch(role!.value.toLowerCase()) { + case PresentationMailbox.inboxRole: + return AppLocalizations.of(context).inboxMailboxDisplayName; + case PresentationMailbox.archiveRole: + return AppLocalizations.of(context).archiveMailboxDisplayName; + case PresentationMailbox.draftsRole: + return AppLocalizations.of(context).draftsMailboxDisplayName; + case PresentationMailbox.sentRole: + return AppLocalizations.of(context).sentMailboxDisplayName; + case PresentationMailbox.outboxRole: + return AppLocalizations.of(context).outboxMailboxDisplayName; + case PresentationMailbox.trashRole: + return AppLocalizations.of(context).trashMailboxDisplayName; + case PresentationMailbox.spamRole: + return AppLocalizations.of(context).spamMailboxDisplayName; + case PresentationMailbox.templatesRole: + return AppLocalizations.of(context).templatesMailboxDisplayName; + } + } + return name?.name ?? ''; + } + + String getMailboxIcon(ImagePaths imagePaths) { + if (hasRole()) { + switch(role!.value) { + case PresentationMailbox.inboxRole: + return imagePaths.icMailboxInbox; + case PresentationMailbox.draftsRole: + return imagePaths.icMailboxDrafts; + case PresentationMailbox.outboxRole: + return imagePaths.icMailboxOutbox; + case PresentationMailbox.archiveRole: + return imagePaths.icMailboxArchived; + case PresentationMailbox.sentRole: + return imagePaths.icMailboxSent; + case PresentationMailbox.trashRole: + return imagePaths.icMailboxTrash; + case PresentationMailbox.spamRole: + return imagePaths.icMailboxSpam; + case PresentationMailbox.templatesRole: + return imagePaths.icMailboxTemplate; + case 'all_mail': + return imagePaths.icMailboxAllMail; + default: + return imagePaths.icFolderMailbox; + } + } else if (isChildOfTeamMailboxes) { + switch(name!.name.toLowerCase()) { + case 'inbox': + return imagePaths.icMailboxInbox; + case 'outbox': + return imagePaths.icMailboxOutbox; + case 'drafts': + return imagePaths.icMailboxDrafts; + case 'archive': + return imagePaths.icMailboxArchived; + case 'sent': + return imagePaths.icMailboxSent; + case 'trash': + return imagePaths.icMailboxTrash; + case 'spam': + return imagePaths.icMailboxSpam; + case 'templates': + return imagePaths.icMailboxTemplate; + default: + return imagePaths.icFolderMailbox; + } + } else { + return imagePaths.icFolderMailbox; + } + } + + Uri get mailboxRouteWeb => RouteUtils.generateRouteBrowser(AppRoutes.dashboard, NavigationRouter(mailboxId: id)); +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mailbox_bindings.dart b/lib/features/mailbox/presentation/mailbox_bindings.dart index ca35e85904..60f17d70b0 100644 --- a/lib/features/mailbox/presentation/mailbox_bindings.dart +++ b/lib/features/mailbox/presentation/mailbox_bindings.dart @@ -1,7 +1,7 @@ import 'package:core/core.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; diff --git a/lib/features/mailbox/presentation/mailbox_controller.dart b/lib/features/mailbox/presentation/mailbox_controller.dart index 9f6aae92bc..9b7fc72a5c 100644 --- a/lib/features/mailbox/presentation/mailbox_controller.dart +++ b/lib/features/mailbox/presentation/mailbox_controller.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:core/core.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -9,6 +8,7 @@ import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; @@ -25,6 +25,7 @@ import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanent import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_subscribe_action_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_subscribe_state.dart'; @@ -51,6 +52,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/rename_mailbox_in import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories_expand_mode.dart'; @@ -67,11 +69,13 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/navigation_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; @@ -93,6 +97,10 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM final SubscribeMultipleMailboxInteractor _subscribeMultipleMailboxInteractor; final currentSelectMode = SelectMode.INACTIVE.obs; + final _activeScrollTop = RxBool(false); + final _activeScrollBottom = RxBool(true); + + MailboxId? _newFolderId; final _openMailboxEventController = StreamController(); final mailboxListScrollController = ScrollController(); @@ -127,11 +135,11 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM @override void onReady() { - _openMailboxEventController.stream.throttleTime(const Duration(milliseconds: 800)).listen((event) { + _openMailboxEventController.stream.debounceTime(const Duration(milliseconds: 500)).listen((event) { _handleOpenMailbox(event.buildContext, event.presentationMailbox); }); - _initCollapseMailboxCategories(); + mailboxListScrollController.addListener(_mailboxListScrollControllerListener); super.onReady(); } @@ -143,62 +151,64 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } @override - void onData(Either newState) { - super.onData(newState); - newState.map((success) async { - if (success is GetAllMailboxSuccess) { - currentMailboxState = success.currentMailboxState; - _buildMailboxTreeHasSubscribed(success.mailboxList); - } else if (success is RefreshChangesAllMailboxSuccess) { - currentMailboxState = success.currentMailboxState; - _refreshMailboxTreeHasSubscribed(success.mailboxList); - } - }); + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetAllMailboxSuccess) { + _handleGetAllMailboxSuccess(success); + } else if (success is RefreshChangesAllMailboxSuccess) { + _handleRefreshChangesAllMailboxSuccess(success); + } else if (success is CreateNewMailboxSuccess) { + _createNewMailboxSuccess(success); + } else if (success is DeleteMultipleMailboxAllSuccess) { + _deleteMultipleMailboxSuccess(success.listMailboxIdDeleted, success.currentMailboxState); + } else if (success is DeleteMultipleMailboxHasSomeSuccess) { + _deleteMultipleMailboxSuccess(success.listMailboxIdDeleted, success.currentMailboxState); + } else if (success is RenameMailboxSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MoveMailboxSuccess) { + _moveMailboxSuccess(success); + } else if (success is SubscribeMailboxSuccess) { + _handleUnsubscribeMailboxSuccess(success); + } else if (success is SubscribeMultipleMailboxAllSuccess) { + _handleUnsubscribeMultipleMailboxAllSuccess(success); + } else if (success is SubscribeMultipleMailboxHasSomeSuccess) { + _handleUnsubscribeMultipleMailboxHasSomeSuccess(success); + } + } + + @override + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is CreateNewMailboxFailure) { + _createNewMailboxFailure(failure); + } else if (failure is DeleteMultipleMailboxFailure) { + _deleteMailboxFailure(failure); + } else if (failure is RefreshChangesAllMailboxFailure) { + _clearNewFolderId(); + } } @override void onDone() { - viewState.value.fold( - (failure) { - if (failure is CreateNewMailboxFailure) { - _createNewMailboxFailure(failure); - } else if (failure is DeleteMultipleMailboxFailure) { - _deleteMailboxFailure(failure); - } - }, - (success) async { - if (success is GetAllMailboxSuccess) { - _initialMailboxVariableStorage(); - } else if (success is RefreshChangesAllMailboxSuccess) { - _initialMailboxVariableStorage(isRefreshChange: true); - } else if (success is CreateNewMailboxSuccess) { - _createNewMailboxSuccess(success); - } else if (success is DeleteMultipleMailboxAllSuccess) { - _deleteMultipleMailboxSuccess(success.listMailboxIdDeleted, success.currentMailboxState); - } else if (success is DeleteMultipleMailboxHasSomeSuccess) { - _deleteMultipleMailboxSuccess(success.listMailboxIdDeleted, success.currentMailboxState); - } else if (success is RenameMailboxSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MoveMailboxSuccess) { - _moveMailboxSuccess(success); - } else if (success is SubscribeMailboxSuccess) { - _handleUnsubscribeMailboxSuccess(success); - } else if (success is SubscribeMultipleMailboxAllSuccess) { - _handleUnsubscribeMultipleMailboxAllSuccess(success); - } else if (success is SubscribeMultipleMailboxHasSomeSuccess) { - _handleUnsubscribeMultipleMailboxHasSomeSuccess(success); + super.onDone(); + viewState.value.fold((failure) => null, (success) { + if (success is GetAllMailboxSuccess) { + _initialMailboxVariableStorage(); + mailboxDashBoardController.getSpamReportBanner(); + } else if (success is RefreshChangesAllMailboxSuccess) { + _initialMailboxVariableStorage(isRefreshChange: true); + mailboxDashBoardController.refreshSpamReportBanner(); + + if (_newFolderId != null) { + _redirectToNewFolder(); } } - ); - } - - void handleScrollEnable() { - isMailboxListScrollable.value = mailboxListScrollController.hasClients && mailboxListScrollController.position.maxScrollExtent > 0; + }); } void _registerObxStreamListener() { ever(mailboxDashBoardController.accountId, (accountId) { - if (accountId is AccountId) { + if (accountId != null && mailboxDashBoardController.sessionCurrent != null) { getAllMailbox(mailboxDashBoardController.sessionCurrent!, accountId); } }); @@ -209,64 +219,68 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM ); ever(mailboxDashBoardController.viewState, (state) { - if (state is Either) { - state.fold((failure) => null, (success) { - if (success is MarkAsMultipleEmailReadAllSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MarkAsMultipleEmailReadHasSomeEmailFailure) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MoveMultipleEmailToMailboxAllSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is DeleteMultipleEmailsPermanentlyAllSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is EmptyTrashFolderSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MarkAsEmailReadSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MoveToMailboxSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is DeleteEmailPermanentlySuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is SaveEmailAsDraftsSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is RemoveEmailDraftsSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is SendEmailSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MarkAsMailboxReadAllSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is MarkAsMailboxReadHasSomeEmailFailure) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } else if (success is UpdateEmailDraftsSuccess) { - _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); - } - }); - } + state.fold((failure) => null, (success) { + if (success is MarkAsMultipleEmailReadAllSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MarkAsMultipleEmailReadHasSomeEmailFailure) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MoveMultipleEmailToMailboxAllSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is DeleteMultipleEmailsPermanentlyAllSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is EmptyTrashFolderSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MarkAsEmailReadSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MoveToMailboxSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is DeleteEmailPermanentlySuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is SaveEmailAsDraftsSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is RemoveEmailDraftsSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is SendEmailSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MarkAsMailboxReadAllSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is MarkAsMailboxReadHasSomeEmailFailure) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is UpdateEmailDraftsSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } else if (success is EmptySpamFolderSuccess) { + _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); + } + }); }); ever(mailboxDashBoardController.dashBoardAction, (action) { if (action is ClearSearchEmailAction) { _switchBackToMailboxDefault(); - } else if (action is OpenMailboxAction) { - openMailbox(action.context, action.presentationMailbox); } }); ever(mailboxDashBoardController.mailboxUIAction, (action) { if (action is SelectMailboxDefaultAction) { - if (mailboxDashBoardController.selectedMailbox.value == null) { - _switchBackToMailboxDefault(); - } + _switchBackToMailboxDefault(); mailboxDashBoardController.clearMailboxUIAction(); } else if (action is RefreshChangeMailboxAction) { if (action.newState != currentMailboxState) { _refreshMailboxChanges(); } mailboxDashBoardController.clearMailboxUIAction(); + } else if (action is OpenMailboxAction) { + if (currentContext != null) { + _handleOpenMailbox(currentContext!, action.presentationMailbox); + if (action.presentationMailbox.role == PresentationMailbox.roleInbox) { + _autoScrollToTopMailboxList(); + } + } + mailboxDashBoardController.clearMailboxUIAction(); } }); } @@ -276,14 +290,14 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM && (_responsiveUtils.isMobile(currentContext!) || _responsiveUtils.isTablet(currentContext!))) { mailboxCategoriesExpandMode.value = MailboxCategoriesExpandMode( defaultMailbox: ExpandMode.COLLAPSE, - personalMailboxes: ExpandMode.COLLAPSE, + personalFolders: ExpandMode.COLLAPSE, teamMailboxes: ExpandMode.COLLAPSE); } else { mailboxCategoriesExpandMode.value = MailboxCategoriesExpandMode.initial(); } } - void refreshAllMailbox() { + Future refreshAllMailbox() async { final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; if (session != null && accountId != null) { @@ -292,7 +306,6 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } void _refreshMailboxChanges({jmap.State? currentMailboxState}) { - mailboxDashBoardController.showSpamReportBanner(); log('MailboxController::_refreshMailboxChanges(): currentMailboxState: $currentMailboxState'); final newMailboxState = currentMailboxState ?? this.currentMailboxState; log('MailboxController::_refreshMailboxChanges(): newMailboxState: $newMailboxState'); @@ -300,6 +313,8 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM final session = mailboxDashBoardController.sessionCurrent; if (accountId != null && session != null && newMailboxState != null) { refreshMailboxChanges(session, accountId, newMailboxState); + } else { + _newFolderId = null; } } @@ -386,6 +401,15 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM log('MailboxController::_handleDataFromNavigationRouter():navigationRouter: $navigationRouter'); if (isHasDataFromRoute) { + if (isRedirectToMailtoURL) { + mailboxDashBoardController.goToComposer( + ComposerArguments.fromMailtoUri( + emailAddress: navigationRouter?.emailAddress, + subject: navigationRouter?.subject + ) + ); + } + if (mailboxIdFromNavigationRouter != null) { _selectMailboxFromRouter(); } else if (emailIdFromNavigationRouter != null) { @@ -407,7 +431,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM NavigationRouter? get navigationRouter => mailboxDashBoardController.navigationRouter; - bool get isHasDataFromRoute => BuildUtils.isWeb && navigationRouter != null; + bool get isHasDataFromRoute => PlatformInfo.isWeb && navigationRouter != null; MailboxId? get mailboxIdFromNavigationRouter => navigationRouter?.mailboxId; @@ -415,6 +439,8 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM SearchQuery? get searchQueryFromNavigationRouter => navigationRouter?.searchQuery; + bool get isRedirectToMailtoURL => navigationRouter?.routeName == AppRoutes.mailtoURL; + void _clearNavigationRouter() { mailboxDashBoardController.navigationRouter = null; } @@ -431,7 +457,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _clearNavigationRouter(); } else { _clearNavigationRouter(); - pushAndPop(AppRoutes.unknownRoutePage); + popAndPush(AppRoutes.unknownRoutePage); } } @@ -439,23 +465,17 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM BuildContext context, PresentationMailbox presentationMailboxSelected ) { - log('MailboxController::_handleOpenMailbox(): '); - FocusScope.of(context).unfocus(); - + log('MailboxController::_handleOpenMailbox():MailboxName: ${presentationMailboxSelected.name}'); + KeyboardUtils.hideKeyboard(context); + mailboxDashBoardController.setSelectedMailbox(presentationMailboxSelected); mailboxDashBoardController.clearSelectedEmail(); if (presentationMailboxSelected.id != mailboxDashBoardController.selectedMailbox.value?.id) { mailboxDashBoardController.clearFilterMessageOption(); } _disableAllSearchEmail(); - - mailboxDashBoardController.setSelectedMailbox(presentationMailboxSelected); _updateSelectedMailboxRouteOnBrowser(); - - if (mailboxDashBoardController.isDrawerOpen) { - mailboxDashBoardController.closeMailboxMenuDrawer(); - } else { - mailboxDashBoardController.dispatchRoute(DashboardRoutes.thread); - } + mailboxDashBoardController.closeMailboxMenuDrawer(); + mailboxDashBoardController.dispatchRoute(DashboardRoutes.thread); } void _disableAllSearchEmail() { @@ -470,62 +490,46 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM _openMailboxEventController.add(OpenMailboxViewEvent(context, presentationMailboxSelected)); } - void goToCreateNewMailboxView(BuildContext context) async { + void goToCreateNewMailboxView(BuildContext context, {PresentationMailbox? parentMailbox}) async { final accountId = mailboxDashBoardController.accountId.value; - if (accountId != null) { + final session = mailboxDashBoardController.sessionCurrent; + if (session !=null && accountId != null) { final arguments = MailboxCreatorArguments( - accountId, - defaultMailboxTree.value, - personalMailboxTree.value, - teamMailboxesTree.value, - mailboxDashBoardController.sessionCurrent!); - - if (BuildUtils.isWeb) { - showDialogMailboxCreator( - context: context, - arguments: arguments, - onCreatedMailbox: (newMailboxArguments) { - final generateCreateId = Id(_uuid.v1()); - _createNewMailboxAction(accountId, CreateNewMailboxRequest( - generateCreateId, - newMailboxArguments.newName, - parentId: newMailboxArguments.mailboxLocation?.id)); - }); - } else { - final newMailboxArguments = await push( - AppRoutes.mailboxCreator, - arguments: arguments); - - if (newMailboxArguments != null && newMailboxArguments is NewMailboxArguments) { - final generateCreateId = Id(_uuid.v1()); - _createNewMailboxAction(accountId, CreateNewMailboxRequest( - generateCreateId, - newMailboxArguments.newName, - parentId: newMailboxArguments.mailboxLocation?.id)); - } + accountId, + defaultMailboxTree.value, + personalMailboxTree.value, + teamMailboxesTree.value, + mailboxDashBoardController.sessionCurrent!, + parentMailbox + ); + + final result = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.mailboxCreator, arguments: arguments) + : await push(AppRoutes.mailboxCreator, arguments: arguments); + + if (result != null && result is NewMailboxArguments) { + final generateCreateId = Id(_uuid.v1()); + _createNewMailboxAction(session, accountId, CreateNewMailboxRequest( + generateCreateId, + result.newName, + parentId: result.mailboxLocation?.id)); } } } - void _createNewMailboxAction(AccountId accountId, CreateNewMailboxRequest request) async { - consumeState(_createNewMailboxInteractor.execute(accountId, request)); + void _createNewMailboxAction(Session session, AccountId accountId, CreateNewMailboxRequest request) async { + consumeState(_createNewMailboxInteractor.execute(session, accountId, request)); } void _createNewMailboxSuccess(CreateNewMailboxSuccess success) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showBottomToast( - currentOverlayContext!, - AppLocalizations.of(currentContext!).new_mailbox_is_created(success.newMailbox.name?.name ?? ''), - leadingIcon: SvgPicture.asset( - _imagePaths.icFolderMailbox, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastSuccessBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!)); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).createFolderSuccessfullyMessage(success.newMailbox.name?.name ?? ''), + leadingSVGIconColor: Colors.white, + leadingSVGIcon: _imagePaths.icFolderMailbox); + + _newFolderId = success.newMailbox.id; } _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); @@ -538,25 +542,12 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM if (exception is ErrorMethodResponse) { messageError = exception.description ?? AppLocalizations.of(currentContext!).create_new_mailbox_failure; } - - _appToast.showBottomToast( - currentOverlayContext!, - messageError, - leadingIcon: SvgPicture.asset( - _imagePaths.icNotConnection, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!)); + _appToast.showToastErrorMessage(currentOverlayContext!, messageError); } } void openSearchViewAction(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { SearchMailboxBindings().dependencies(); mailboxDashBoardController.searchMailboxActivated.value = true; } else { @@ -575,6 +566,31 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM bool isSelectionEnabled() => currentSelectMode.value == SelectMode.ACTIVE; + List get listActionOfMailboxSelected { + final currentMailboxesSelected = listMailboxSelected; + + if (currentMailboxesSelected.length == 1) { + if (currentMailboxesSelected.isAllDefaultMailboxes && currentMailboxesSelected.isAllUnreadMailboxes) { + return [MailboxActions.markAsRead]; + } else if (currentMailboxesSelected.isAllPersonalMailboxes) { + return [ + MailboxActions.move, + MailboxActions.rename, + if (currentMailboxesSelected.isAllUnreadMailboxes) + MailboxActions.markAsRead, + MailboxActions.delete + ]; + } else { + return []; + } + } else if (currentMailboxesSelected.length > 1 + && currentMailboxesSelected.isAllPersonalMailboxes) { + return [MailboxActions.delete]; + } else { + return []; + } + } + void _cancelSelectMailbox() { unAllSelectedMailboxNode(); currentSelectMode.value = SelectMode.INACTIVE; @@ -626,7 +642,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM context, selectedMailboxList.first, mailboxDashBoardController, - onMovingMailboxAction: _invokeMovingMailboxAction + onMovingMailboxAction: (mailboxSelected, destinationMailbox) => _invokeMovingMailboxAction(context, mailboxSelected, destinationMailbox) ); break; default: @@ -664,11 +680,11 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM jmap.State? currentMailboxState ) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - message: AppLocalizations.of(currentContext!).delete_mailboxes_successfully, - icon: _imagePaths.icSelected); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).delete_mailboxes_successfully); } + if (listMailboxIdDeleted.contains(selectedMailbox?.id)) { _switchBackToMailboxDefault(); _closeEmailViewIfMailboxDisabledOrNotExist(listMailboxIdDeleted); @@ -743,26 +759,26 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM final inboxMailbox = findMailboxNodeByRole(PresentationMailbox.roleInbox); mailboxDashBoardController.setSelectedMailbox(inboxMailbox?.item); _updateSelectedMailboxRouteOnBrowser(); - mailboxListScrollController.animateTo( - 0, - duration: const Duration(milliseconds: 500), - curve: Curves.fastOutSlowIn); + _autoScrollToTopMailboxList(); } void _deleteMailboxFailure(DeleteMultipleMailboxFailure failure) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - message: AppLocalizations.of(currentContext!).delete_mailboxes_failure, - icon: _imagePaths.icDeleteToast); + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).delete_mailboxes_failure, + leadingSVGIcon: _imagePaths.icDeleteToast + ); } } void _renameMailboxAction(PresentationMailbox presentationMailbox, MailboxName newMailboxName) { final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; - if (accountId != null) { + if (session != null && accountId != null) { consumeState(_renameMailboxInteractor.execute( + session, accountId, RenameMailboxRequest(presentationMailbox.id, newMailboxName)) ); @@ -772,27 +788,32 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } void _handleMovingMailbox( - AccountId accountId, - MoveAction moveAction, - PresentationMailbox mailboxSelected, - {PresentationMailbox? destinationMailbox}) { - consumeState(_moveMailboxInteractor.execute(accountId, - MoveMailboxRequest( - mailboxSelected.id, - moveAction, - destinationMailboxId: destinationMailbox?.id, - destinationMailboxName: destinationMailbox?.name, - parentId: mailboxSelected.parentId))); + BuildContext context, + Session session, + AccountId accountId, + MoveAction moveAction, + PresentationMailbox mailboxSelected, + {PresentationMailbox? destinationMailbox} + ) { + consumeState(_moveMailboxInteractor.execute( + session, + accountId, + MoveMailboxRequest( + mailboxSelected.id, + moveAction, + destinationMailboxId: destinationMailbox?.id, + destinationMailboxDisplayName: destinationMailbox?.getDisplayName(context), + parentId: mailboxSelected.parentId))); } void _moveMailboxSuccess(MoveMailboxSuccess success) { if (success.moveAction == MoveAction.moving && currentOverlayContext != null && currentContext != null) { - _appToast.showBottomToast( + _appToast.showToastMessage( currentOverlayContext!, AppLocalizations.of(currentContext!).moved_to_mailbox( - success.destinationMailboxName?.name ?? AppLocalizations.of(currentContext!).allMailboxes), + success.destinationMailboxDisplayName ?? AppLocalizations.of(currentContext!).allMailboxes), actionName: AppLocalizations.of(currentContext!).undo, onActionClick: () { _undoMovingMailbox(MoveMailboxRequest( @@ -801,45 +822,46 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM destinationMailboxId: success.parentId, parentId: success.destinationMailboxId)); }, - leadingIcon: SvgPicture.asset( - _imagePaths.icFolderMailbox, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), + leadingSVGIcon: _imagePaths.icFolderMailbox, + leadingSVGIconColor: Colors.white, backgroundColor: AppColor.toastSuccessBackgroundColor, textColor: Colors.white, - textActionColor: Colors.white, - actionIcon: SvgPicture.asset(_imagePaths.icUndo), - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!)); + actionIcon: SvgPicture.asset(_imagePaths.icUndo)); } _refreshMailboxChanges(currentMailboxState: success.currentMailboxState); } void _undoMovingMailbox(MoveMailboxRequest newMoveRequest) { + final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; - if (accountId != null) { - consumeState(_moveMailboxInteractor.execute(accountId, newMoveRequest)); + if (session != null && accountId != null) { + consumeState(_moveMailboxInteractor.execute(session, accountId, newMoveRequest)); } } - void toggleMailboxCategories(MailboxCategories categories) { + void toggleMailboxCategories(MailboxCategories categories) async { switch(categories) { case MailboxCategories.exchange: final newExpandMode = mailboxCategoriesExpandMode.value.defaultMailbox == ExpandMode.EXPAND ? ExpandMode.COLLAPSE : ExpandMode.EXPAND; mailboxCategoriesExpandMode.value.defaultMailbox = newExpandMode; mailboxCategoriesExpandMode.refresh(); break; - case MailboxCategories.personalMailboxes: - final newExpandMode = mailboxCategoriesExpandMode.value.personalMailboxes == ExpandMode.EXPAND ? ExpandMode.COLLAPSE : ExpandMode.EXPAND; - mailboxCategoriesExpandMode.value.personalMailboxes = newExpandMode; + case MailboxCategories.personalFolders: + final newExpandMode = mailboxCategoriesExpandMode.value.personalFolders == ExpandMode.EXPAND ? ExpandMode.COLLAPSE : ExpandMode.EXPAND; + mailboxCategoriesExpandMode.value.personalFolders = newExpandMode; mailboxCategoriesExpandMode.refresh(); + if (personalMailboxTree.value.root.hasChildren()) { + _triggerToggleMailboxCategories(); + } break; case MailboxCategories.teamMailboxes: final newExpandMode = mailboxCategoriesExpandMode.value.teamMailboxes == ExpandMode.EXPAND ? ExpandMode.COLLAPSE : ExpandMode.EXPAND; mailboxCategoriesExpandMode.value.teamMailboxes = newExpandMode; mailboxCategoriesExpandMode.refresh(); + if (personalMailboxTree.value.root.hasChildren() && mailboxCategoriesExpandMode.value.teamMailboxes == ExpandMode.COLLAPSE) { + _triggerToggleMailboxCategories(); + } break; case MailboxCategories.appGrid: final currentExpandMode = mailboxDashBoardController.appGridDashboardController.appDashboardExpandMode.value; @@ -852,6 +874,14 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } } + void _triggerToggleMailboxCategories() async { + await Future.delayed(const Duration(milliseconds: 200)); + mailboxListScrollController.animateTo( + mailboxListScrollController.offset + 100, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInToLinear); + } + void _handleNavigationRouteParameters(Map? parameters) { log('MailboxController::_handleNavigationRouteParameters(): parameters: $parameters'); if (parameters != null) { @@ -895,7 +925,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM context, mailbox, mailboxDashBoardController, - onMovingMailboxAction: _invokeMovingMailboxAction + onMovingMailboxAction: (mailboxSelected, destinationMailbox) => _invokeMovingMailboxAction(context, mailboxSelected, destinationMailbox) ); break; case MailboxActions.markAsRead: @@ -916,15 +946,31 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM case MailboxActions.disableMailbox: _unsubscribeMailboxAction(mailbox.id); break; + case MailboxActions.emptyTrash: + emptyTrashAction(context, mailbox, mailboxDashBoardController); + break; + case MailboxActions.emptySpam: + emptySpamAction(context, mailbox, mailboxDashBoardController); + break; + case MailboxActions.newSubfolder: + goToCreateNewMailboxView(context, parentMailbox: mailbox); + break; default: break; } } - void _invokeMovingMailboxAction(PresentationMailbox mailboxSelected, PresentationMailbox? destinationMailbox) { + void _invokeMovingMailboxAction( + BuildContext context, + PresentationMailbox mailboxSelected, + PresentationMailbox? destinationMailbox + ) { final accountId = mailboxDashBoardController.accountId.value; - if (accountId != null) { + final session = mailboxDashBoardController.sessionCurrent; + if (session != null && accountId != null) { _handleMovingMailbox( + context, + session, accountId, MoveAction.moving, mailboxSelected, @@ -935,7 +981,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } void _updateSelectedMailboxRouteOnBrowser() { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { final selectedMailboxId = selectedMailbox?.id; final route = RouteUtils.generateRouteBrowser( AppRoutes.dashboard, @@ -979,20 +1025,28 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM curve: Curves.fastOutSlowIn); } - void _buildMailboxTreeHasSubscribed(List mailboxList) async { - final _mailboxList = mailboxList.listSubscribedMailboxes; - await buildTree(_mailboxList); + void _handleGetAllMailboxSuccess(GetAllMailboxSuccess success) async { + currentMailboxState = success.currentMailboxState; + final listMailboxDisplayed = success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes; + await buildTree(listMailboxDisplayed); + if (currentContext != null) { + await syncAllMailboxWithDisplayName(currentContext!); + } } - void _refreshMailboxTreeHasSubscribed(List mailboxList) async { - final _mailboxList = mailboxList.listSubscribedMailboxes; - await refreshTree(_mailboxList); + void _handleRefreshChangesAllMailboxSuccess(RefreshChangesAllMailboxSuccess success) async { + currentMailboxState = success.currentMailboxState; + final listMailboxDisplayed = success.mailboxList.listSubscribedMailboxesAndDefaultMailboxes; + await refreshTree(listMailboxDisplayed); + if (currentContext != null) { + await syncAllMailboxWithDisplayName(currentContext!); + } } void _unsubscribeMailboxAction(MailboxId mailboxId) { - final _accountId = mailboxDashBoardController.accountId.value; - - if (_accountId != null) { + final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; + if (session != null && accountId != null) { final subscribeRequest = generateSubscribeRequest( mailboxId, MailboxSubscribeState.disabled, @@ -1000,9 +1054,9 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM ); if (subscribeRequest is SubscribeMultipleMailboxRequest) { - consumeState(_subscribeMultipleMailboxInteractor.execute(_accountId, subscribeRequest)); + consumeState(_subscribeMultipleMailboxInteractor.execute(session, accountId, subscribeRequest)); } else if (subscribeRequest is SubscribeMailboxRequest) { - consumeState(_subscribeMailboxInteractor.execute(_accountId, subscribeRequest)); + consumeState(_subscribeMailboxInteractor.execute(session, accountId, subscribeRequest)); } } } @@ -1069,7 +1123,7 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM {List? listDescendantMailboxIds} ) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showBottomToast( + _appToast.showToastMessage( currentOverlayContext!, AppLocalizations.of(currentContext!).toastMsgHideMailboxSuccess, actionName: AppLocalizations.of(currentContext!).undo, @@ -1077,19 +1131,11 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM mailboxIdSubscribed, listDescendantMailboxIds: listDescendantMailboxIds ), - leadingIcon: SvgPicture.asset( - _imagePaths.icFolderMailbox, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill - ), + leadingSVGIcon: _imagePaths.icFolderMailbox, + leadingSVGIconColor: Colors.white, backgroundColor: AppColor.toastSuccessBackgroundColor, textColor: Colors.white, - textActionColor: Colors.white, - actionIcon: SvgPicture.asset(_imagePaths.icUndo), - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!) - ); + actionIcon: SvgPicture.asset(_imagePaths.icUndo)); } } @@ -1097,9 +1143,10 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM MailboxId mailboxIdSubscribed, {List? listDescendantMailboxIds} ) { - final _accountId = mailboxDashBoardController.accountId.value; + final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; - if (_accountId != null) { + if (session != null && accountId != null) { SubscribeRequest? subscribeRequest; if (listDescendantMailboxIds != null) { @@ -1118,10 +1165,83 @@ class MailboxController extends BaseMailboxController with MailboxActionHandlerM } if (subscribeRequest is SubscribeMultipleMailboxRequest) { - consumeState(_subscribeMultipleMailboxInteractor.execute(_accountId, subscribeRequest)); + consumeState(_subscribeMultipleMailboxInteractor.execute(session, accountId, subscribeRequest)); } else if (subscribeRequest is SubscribeMailboxRequest) { - consumeState(_subscribeMailboxInteractor.execute(_accountId, subscribeRequest)); + consumeState(_subscribeMailboxInteractor.execute(session, accountId, subscribeRequest)); + } + } + } + + void _mailboxListScrollControllerListener() { + _handleScrollTop(); + _handleScrollBottom(); + } + + void _handleScrollTop() { + if (mailboxListScrollController.position.pixels == 0) { + _activeScrollTop.value = false; + } + + if (mailboxListScrollController.position.pixels > 40) { + _activeScrollTop.value = true; + } + } + + void _handleScrollBottom() { + if (mailboxListScrollController.position.pixels - mailboxListScrollController.position.maxScrollExtent == 0) { + _activeScrollBottom.value = false; + } + + if (mailboxListScrollController.position.maxScrollExtent - mailboxListScrollController.position.pixels > 40) { + _activeScrollBottom.value = true; + } + } + + bool get activeScrollTop => _activeScrollTop.value; + + bool get activeScrollBottom => _activeScrollBottom.value; + + void openSendingQueueViewAction(BuildContext context) { + KeyboardUtils.hideKeyboard(context); + _disableAllSearchEmail(); + mailboxDashBoardController.clearSelectedEmail(); + mailboxDashBoardController.clearFilterMessageOption(); + mailboxDashBoardController.setSelectedMailbox(null); + closeMailboxScreen(context); + mailboxDashBoardController.dispatchRoute(DashboardRoutes.sendingQueue); + } + + void _clearNewFolderId() { + _newFolderId = null; + } + + void _redirectToNewFolder() { + final newMailboxNode = findMailboxNodeById(_newFolderId!); + log('MailboxController::_redirectToNewFolder:newMailboxNode: $newMailboxNode'); + if (newMailboxNode != null && currentContext != null) { + _handleOpenMailbox(currentContext!, newMailboxNode.item); + } + _clearNewFolderId(); + } + + void _autoScrollToTopMailboxList() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mailboxListScrollController.hasClients){ + mailboxListScrollController.animateTo( + 0, + duration: const Duration(milliseconds: 500), + curve: Curves.fastOutSlowIn + ); } + }); + } + + void emptyMailboxAction(BuildContext context, PresentationMailbox presentationMailbox) { + log('MailboxController::emptyMailboxAction:presentationMailbox: ${presentationMailbox.name}'); + if (presentationMailbox.isTrash) { + mailboxDashBoardController.emptyTrashFolderAction(trashFolderId: presentationMailbox.id); + } else if (presentationMailbox.isSpam) { + mailboxDashBoardController.emptySpamFolderAction(spamFolderId: presentationMailbox.id); } } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mailbox_view.dart b/lib/features/mailbox/presentation/mailbox_view.dart index 6ea660b46e..c432d9272d 100644 --- a/lib/features/mailbox/presentation/mailbox_view.dart +++ b/lib/features/mailbox/presentation/mailbox_view.dart @@ -1,24 +1,24 @@ import 'package:core/core.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_controller.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/base_mailbox_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/widgets/bottom_bar_selection_mailbox_widget.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/widgets/user_information_widget_builder.dart'; -import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_footer_widget.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_item_widget.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_loading_bar_widget.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/sending_queue_mailbox_widget.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/user_information_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; +import 'package:tmail_ui_user/features/quotas/presentation/quotas_view.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_config.dart'; -class MailboxView extends GetWidget with MailboxWidgetMixin { - - final _imagePaths = Get.find(); - final _responsiveUtils = Get.find(); +class MailboxView extends BaseMailboxView { MailboxView({Key? key}) : super(key: key); @@ -27,9 +27,9 @@ class MailboxView extends GetWidget with MailboxWidgetMixin { ThemeUtils.setStatusBarTransparentColor(); return SafeArea(bottom: false, left: false, right: false, - top: _responsiveUtils.isMobile(context), + top: responsiveUtils.isMobile(context), child: ClipRRect( - borderRadius: _responsiveUtils.isPortraitMobile(context) + borderRadius: responsiveUtils.isPortraitMobile(context) ? const BorderRadius.only( topRight: Radius.circular(14), topLeft: Radius.circular(14)) @@ -47,7 +47,7 @@ class MailboxView extends GetWidget with MailboxWidgetMixin { color: Colors.white, child: RefreshIndicator( color: AppColor.primaryColor, - onRefresh: () async => controller.refreshAllMailbox(), + onRefresh: controller.refreshAllMailbox, child: SafeArea( top: false, right: false, @@ -56,21 +56,39 @@ class MailboxView extends GetWidget with MailboxWidgetMixin { ) ), )), - Obx(() => controller.isSelectionEnabled() - ? _buildOptionSelectionMailbox(context) - : const SizedBox.shrink()), + Obx(() { + if (controller.isSelectionEnabled() + && controller.listActionOfMailboxSelected.isNotEmpty) { + return SafeArea( + right: false, + top: false, + child: BottomBarSelectionMailboxWidget( + controller.listMailboxSelected, + controller.listActionOfMailboxSelected, + onMailboxActionsClick: (actions, listMailboxSelected) { + return controller.pressMailboxSelectionAction( + context, + actions, + listMailboxSelected + ); + } + ) + ); + } else { + return const SizedBox.shrink(); + } + }), ]), ), - Obx(() => controller.isMailboxListScrollable.isTrue - && !controller.isSelectionEnabled() - ? const QuotasFooterWidget() - : const SizedBox.shrink(), + Obx(() => !controller.isSelectionEnabled() + ? const QuotasView() + : const SizedBox.shrink(), ), Obx(() { final appInformation = controller.mailboxDashBoardController.appInformation.value; if (appInformation != null && !controller.isSelectionEnabled()) { - if (_responsiveUtils.isLandscapeMobile(context)) { + if (responsiveUtils.isLandscapeMobile(context)) { return const SizedBox.shrink(); } return _buildVersionInformation(context, appInformation); @@ -90,13 +108,13 @@ class MailboxView extends GetWidget with MailboxWidgetMixin { crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: EdgeInsets.only(top: _responsiveUtils.isMobile(context) ? 10 : 30, bottom: 8), + padding: EdgeInsets.only(top: responsiveUtils.isMobile(context) ? 10 : 30, bottom: 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( - padding: const EdgeInsets.only(left: 10), - child: _buildCloseScreenButton(context)), + padding: const EdgeInsetsDirectional.only(start: 10), + child: _buildCloseScreenButton(context)), SizedBox(width: controller.isSelectionEnabled() ? 49 : 40), Expanded(child: Text( AppLocalizations.of(context).folders, @@ -106,22 +124,22 @@ class MailboxView extends GetWidget with MailboxWidgetMixin { ] ) ), - if (!_responsiveUtils.isTabletLarge(context)) - const Divider(color: AppColor.colorDividerMailbox, height: 0.5, thickness: 0.2), + if (!responsiveUtils.isTabletLarge(context)) + const Divider(color: AppColor.colorDividerMailbox, height: 1), ] ); } Widget _buildCloseScreenButton(BuildContext context) { return buildIconWeb( - icon: SvgPicture.asset(_imagePaths.icCloseMailbox, width: 28, height: 28, fit: BoxFit.fill), + icon: SvgPicture.asset(imagePaths.icCircleClose, width: 28, height: 28, fit: BoxFit.fill), tooltip: AppLocalizations.of(context).close, onTap: () => controller.closeMailboxScreen(context)); } Widget _buildEditMailboxButton(BuildContext context, bool isSelectionEnabled) { return Padding( - padding: const EdgeInsets.only(right: 10), + padding: const EdgeInsetsDirectional.only(end: 10), child: Material( shape: const CircleBorder(), color: Colors.transparent, @@ -137,148 +155,137 @@ class MailboxView extends GetWidget with MailboxWidgetMixin { ); } - Widget _buildUserInformation(BuildContext context) { - return Column(children: [ - Padding( - padding: EdgeInsets.only( - left: _responsiveUtils.isLandscapeMobile(context) ? 0 : 16, - right: 16), - child: UserInformationWidgetBuilder( - _imagePaths, - controller.mailboxDashBoardController.userProfile.value, - subtitle: AppLocalizations.of(context).manage_account, - onSubtitleClick: () => controller.mailboxDashBoardController.goToSettings())), - const Divider(color: AppColor.colorDividerMailbox, height: 0.5, thickness: 0.2) - ]); - } - - Widget _buildLoadingView() { - return Obx(() => controller.viewState.value.fold( - (failure) => const SizedBox.shrink(), - (success) => success is LoadingState - ? const Center(child: Padding( - padding: EdgeInsets.only(top: 16), - child: SizedBox( - width: 24, - height: 24, - child: CupertinoActivityIndicator(color: AppColor.colorTextButton)))) - : const SizedBox.shrink())); - } - Widget _buildListMailbox(BuildContext context) { - return NotificationListener( - onNotification: (_) { - controller.handleScrollEnable(); - return true; - }, - child: SingleChildScrollView( - controller: controller.mailboxListScrollController, - key: const PageStorageKey('mailbox_list'), - physics: const ClampingScrollPhysics(), - padding: const EdgeInsets.only(bottom: 16), - child: Column(children: [ - Obx(() { - if (controller.isSelectionEnabled() && _responsiveUtils.isLandscapeMobile(context)) { - return const SizedBox.shrink(); - } - return _buildUserInformation(context); - }), - _buildLoadingView(), - Obx(() { - if (controller.defaultMailboxIsNotEmpty) { - return Padding( - padding: const EdgeInsets.only(top: 16), - child: _buildMailboxCategory( - context, - MailboxCategories.exchange, - controller.defaultRootNode - ), - ); - } else { - return const SizedBox.shrink(); - } - }), - const Divider(color: AppColor.colorDividerMailbox, height: 0.5, thickness: 0.2), - const SizedBox(height: 12), - Container( - margin: EdgeInsets.only( - left: _responsiveUtils.isLandscapeMobile(context) ? 0 : 8, - right: 16), - padding: const EdgeInsets.only(left: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(AppLocalizations.of(context).mailBoxes, + return SingleChildScrollView( + controller: controller.mailboxListScrollController, + key: const PageStorageKey('mailbox_list'), + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.only(bottom: 16), + child: Column(children: [ + Obx(() { + if (controller.isSelectionEnabled() && responsiveUtils.isLandscapeMobile(context)) { + return const SizedBox.shrink(); + } + return UserInformationWidget( + userProfile: controller.mailboxDashBoardController.userProfile.value, + subtitle: AppLocalizations.of(context).manage_account, + onSubtitleClick: controller.mailboxDashBoardController.goToSettings, + border: const Border( + bottom: BorderSide( + color: AppColor.colorDividerHorizontal, + width: 0.5, + ) + ), + ); + }), + Obx(() => MailboxLoadingBarWidget(viewState: controller.viewState.value)), + AppConfig.appGridDashboardAvailable + ? buildAppGridDashboard(context, responsiveUtils, imagePaths, controller) + : const SizedBox.shrink(), + const SizedBox(height: 8), + Obx(() { + if (controller.defaultMailboxIsNotEmpty) { + return _buildMailboxCategory( + context, + MailboxCategories.exchange, + controller.defaultRootNode + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.mailboxDashBoardController.listSendingEmails.isNotEmpty && + PlatformInfo.isMobile && + !controller.isSelectionEnabled()) { + return SendingQueueMailboxWidget( + listSendingEmails: controller.mailboxDashBoardController.listSendingEmails, + onOpenSendingQueueAction: () => controller.openSendingQueueViewAction(context), + isSelected: controller.mailboxDashBoardController.dashboardRoute.value == DashboardRoutes.sendingQueue, + ); + } else { + return const SizedBox.shrink(); + } + }), + const SizedBox(height: 8), + const Divider(color: AppColor.colorDividerMailbox, height: 1), + const SizedBox(height: 12), + Container( + margin: EdgeInsetsDirectional.only( + start: responsiveUtils.isLandscapeMobile(context) ? 0 : 8, + end: 16), + padding: const EdgeInsetsDirectional.only(start: 8), + child: Row( + children: [ + Expanded( + child: Text( + AppLocalizations.of(context).mailBoxes, style: const TextStyle( fontSize: 20, color: Colors.black, - fontWeight: FontWeight.bold)), - Row( - children: [ - buildIconWeb( - minSize: 40, - iconPadding: EdgeInsets.zero, - icon: SvgPicture.asset( - _imagePaths.icSearchBar, - color: AppColor.colorTextButton, - fit: BoxFit.fill - ), - tooltip: AppLocalizations.of(context).searchForMailboxes, - onTap: () => controller.openSearchViewAction(context) - ), - buildIconWeb( - minSize: 40, - iconSize: 20, - iconPadding: EdgeInsets.zero, - splashRadius: 15, - icon: SvgPicture.asset(_imagePaths.icAddNewFolder, color: AppColor.colorTextButton, fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).new_mailbox, - onTap: () => controller.goToCreateNewMailboxView(context)), - ], + fontWeight: FontWeight.bold + ) + ), + ), + Row(children: [ + buildIconWeb( + minSize: 40, + iconPadding: EdgeInsets.zero, + icon: SvgPicture.asset( + imagePaths.icSearchBar, + colorFilter: AppColor.colorTextButton.asFilter(), + fit: BoxFit.fill ), - ]), - ), - const SizedBox(height: 8), - Obx(() { - if (controller.personalMailboxIsNotEmpty) { - return _buildMailboxCategory( - context, - MailboxCategories.personalMailboxes, - controller.personalRootNode - ); - } else { - return const SizedBox.shrink(); - } - }), - const SizedBox(height: 8), - Obx(() { - if (controller.teamMailboxesIsNotEmpty) { - return _buildMailboxCategory( - context, - MailboxCategories.teamMailboxes, - controller.teamMailboxesRootNode - ); - } else { - return const SizedBox.shrink(); - } - }), - Obx(() { - if (controller.isMailboxListScrollable.isFalse) { - return const QuotasFooterWidget(); - } else { - return const SizedBox.shrink(); - } - }), - ]) - ), + tooltip: AppLocalizations.of(context).searchForMailboxes, + onTap: () => controller.openSearchViewAction(context) + ), + buildIconWeb( + minSize: 40, + iconSize: 20, + iconPadding: EdgeInsets.zero, + splashRadius: 15, + icon: SvgPicture.asset( + imagePaths.icAddNewFolder, + colorFilter: AppColor.colorTextButton.asFilter(), + fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).new_mailbox, + onTap: () => controller.goToCreateNewMailboxView(context)), + ]), + ]), + ), + const SizedBox(height: 8), + Obx(() { + if (controller.personalMailboxIsNotEmpty) { + return _buildMailboxCategory( + context, + MailboxCategories.personalFolders, + controller.personalRootNode + ); + } else { + return const SizedBox.shrink(); + } + }), + const SizedBox(height: 8), + Obx(() { + if (controller.teamMailboxesIsNotEmpty) { + return _buildMailboxCategory( + context, + MailboxCategories.teamMailboxes, + controller.teamMailboxesRootNode + ); + } else { + return const SizedBox.shrink(); + } + }), + ]) ); } Widget _buildHeaderMailboxCategory(BuildContext context, MailboxCategories categories) { return Padding( - padding: EdgeInsets.only( - right: _responsiveUtils.isLandscapeMobile(context) ? 8 : 28, - left: 4), + padding: EdgeInsetsDirectional.only( + start: 4, + end: responsiveUtils.isLandscapeMobile(context) ? 8 : 28), child: Row(children: [ buildIconWeb( minSize: 40, @@ -287,9 +294,10 @@ class MailboxView extends GetWidget with MailboxWidgetMixin { splashRadius: 15, icon: SvgPicture.asset( categories.getExpandMode(controller.mailboxCategoriesExpandMode.value) == ExpandMode.EXPAND - ? _imagePaths.icExpandFolder - : _imagePaths.icCollapseFolder, - color: AppColor.primaryColor, fit: BoxFit.fill), + ? imagePaths.icArrowBottom + : DirectionUtils.isDirectionRTLByLanguage(context) ? imagePaths.icArrowLeft : imagePaths.icArrowRight, + colorFilter: AppColor.primaryColor.asFilter(), + fit: BoxFit.fill), tooltip: AppLocalizations.of(context).collapse, onTap: () => controller.toggleMailboxCategories(categories)), Expanded(child: Text(categories.getTitle(context), @@ -303,10 +311,11 @@ class MailboxView extends GetWidget with MailboxWidgetMixin { final lastNode = mailboxNode.childrenItems?.last; return Container( - margin: EdgeInsets.only( - left: _responsiveUtils.isLandscapeMobile(context) ? 0 : 8, - right: 16), - padding: const EdgeInsets.only(left: 12), + margin: EdgeInsetsDirectional.only( + start: responsiveUtils.isLandscapeMobile(context) ? 0 : 8, + end: 16 + ), + padding: const EdgeInsetsDirectional.only(start: 12), child: TreeView( key: Key('${categories.keyValue}_mailbox_list'), children: _buildListChildTileWidget(context, mailboxNode, lastNode: lastNode))); @@ -334,73 +343,38 @@ class MailboxView extends GetWidget with MailboxWidgetMixin { context, key: const Key('children_tree_mailbox_child'), isExpanded: mailboxNode.expandMode == ExpandMode.EXPAND, - parent: Obx(() => (MailBoxFolderTileBuilder( - context, - _imagePaths, - mailboxNode, - lastNode: lastNode, - allSelectMode: controller.currentSelectMode.value) - ..addOnLongPressMailboxNodeAction((mailboxNode) { - openMailboxMenuActionOnMobile( - context, - _imagePaths, - mailboxNode.item, - controller - ); - }) - ..addOnClickOpenMailboxNodeAction((mailboxNode) => controller.openMailbox(context, mailboxNode.item)) - ..addOnClickExpandMailboxNodeAction((mailboxNode) => controller.toggleMailboxFolder(mailboxNode)) - ..addOnSelectMailboxNodeAction((mailboxNode) => controller.selectMailboxNode(mailboxNode)) - ).build()), + paddingChild: const EdgeInsetsDirectional.only(start: 14), + parent: Obx(() => MailboxItemWidget( + mailboxNode: mailboxNode, + selectionMode: controller.currentSelectMode.value, + mailboxNodeSelected: controller.mailboxDashBoardController.selectedMailbox.value, + onLongPressMailboxNodeAction: (mailboxNode) => openMailboxMenuActionOnMobile(context, imagePaths, mailboxNode.item, controller), + onOpenMailboxFolderClick: (mailboxNode) => controller.openMailbox(context, mailboxNode.item), + onExpandFolderActionClick: (mailboxNode) => controller.toggleMailboxFolder(mailboxNode, controller.mailboxListScrollController), + onSelectMailboxFolderClick: (mailboxNode) => controller.selectMailboxNode(mailboxNode), + )), children: _buildListChildTileWidget(context, mailboxNode) ).build(); } else { - return Obx(() => (MailBoxFolderTileBuilder( - context, - _imagePaths, - mailboxNode, - lastNode: lastNode, - allSelectMode: controller.currentSelectMode.value) - ..addOnLongPressMailboxNodeAction((mailboxNode) { - openMailboxMenuActionOnMobile( - context, - _imagePaths, - mailboxNode.item, - controller - ); - }) - ..addOnClickOpenMailboxNodeAction((mailboxNode) => controller.openMailbox(context, mailboxNode.item)) - ..addOnSelectMailboxNodeAction((mailboxNode) => controller.selectMailboxNode(mailboxNode)) - ).build()); + return Obx(() => MailboxItemWidget( + mailboxNode: mailboxNode, + selectionMode: controller.currentSelectMode.value, + mailboxNodeSelected: controller.mailboxDashBoardController.selectedMailbox.value, + onLongPressMailboxNodeAction: (mailboxNode) => openMailboxMenuActionOnMobile(context, imagePaths, mailboxNode.item, controller), + onOpenMailboxFolderClick: (mailboxNode) => controller.openMailbox(context, mailboxNode.item), + onSelectMailboxFolderClick: (mailboxNode) => controller.selectMailboxNode(mailboxNode) + )); } }).toList() ?? []; } - Widget _buildOptionSelectionMailbox(BuildContext context) { - return Column(children: [ - const Divider(color: AppColor.lineItemListColor, height: 1, thickness: 0.2), - SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: (BottomBarSelectionMailboxWidget(context, - _imagePaths, - controller.listMailboxSelected) - ..addOnMailboxActionsClick((actions, listMailboxSelected) => - controller.pressMailboxSelectionAction( - context, - actions, - listMailboxSelected))) - .build())) - ]); - } - Widget _buildVersionInformation(BuildContext context, PackageInfo packageInfo) { - return SafeArea( - top: false, - child: Container( - color: AppColor.colorBgMailbox, - width: double.infinity, - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + return Container( + color: AppColor.colorBgMailbox, + width: double.infinity, + padding: const EdgeInsets.all(16), + child: SafeArea( + top: false, child: Text( '${AppLocalizations.of(context).version} ${packageInfo.version}', textAlign: TextAlign.center, diff --git a/lib/features/mailbox/presentation/mailbox_view_web.dart b/lib/features/mailbox/presentation/mailbox_view_web.dart index 4624204bbf..f30f0eeaa9 100644 --- a/lib/features/mailbox/presentation/mailbox_view_web.dart +++ b/lib/features/mailbox/presentation/mailbox_view_web.dart @@ -1,61 +1,52 @@ import 'package:core/core.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_controller.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/base_mailbox_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/widgets/user_information_widget_builder.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_list_dashboard_item.dart'; -import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_footer_widget.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_item_widget.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_loading_bar_widget.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/user_information_widget.dart'; +import 'package:tmail_ui_user/features/quotas/presentation/quotas_view.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; -class MailboxView extends GetWidget - with AppLoaderMixin, - MailboxWidgetMixin { - - final _imagePaths = Get.find(); - final _responsiveUtils = Get.find(); +class MailboxView extends BaseMailboxView { MailboxView({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return Drawer( - elevation: _responsiveUtils.isDesktop(context) ? 0 : 16.0, + elevation: responsiveUtils.isDesktop(context) ? 0 : 16.0, child: Scaffold( - backgroundColor: Colors.white, + backgroundColor: responsiveUtils.isWebDesktop(context) + ? AppColor.colorBgDesktop + : Colors.white, body: Column(children: [ - if (!_responsiveUtils.isDesktop(context)) _buildLogoApp(context), - if (!_responsiveUtils.isDesktop(context)) + if (!responsiveUtils.isDesktop(context)) _buildLogoApp(context), + if (!responsiveUtils.isDesktop(context)) const Divider( color: AppColor.colorDividerMailbox, height: 0.5, thickness: 0.2), Expanded(child: Container( - padding: EdgeInsets.only( - left: _responsiveUtils.isDesktop(context) ? 16 : 0), - color: _responsiveUtils.isDesktop(context) + padding: EdgeInsetsDirectional.only(start: responsiveUtils.isDesktop(context) ? 16 : 0), + color: responsiveUtils.isDesktop(context) ? AppColor.colorBgDesktop : Colors.white, child: Container( - color: _responsiveUtils.isDesktop(context) + color: responsiveUtils.isDesktop(context) ? AppColor.colorBgDesktop : Colors.white, - child: RefreshIndicator( - color: AppColor.primaryColor, - onRefresh: () async => controller.refreshAllMailbox(), - child: _buildListMailbox(context) - ), + child: _buildListMailbox(context), ), )), - const QuotasFooterWidget(), + const QuotasView(), ]), ) ); @@ -64,18 +55,19 @@ class MailboxView extends GetWidget Widget _buildLogoApp(BuildContext context) { return Container( color: Colors.white, - padding: EdgeInsets.only( - top: _responsiveUtils.isDesktop(context) ? 25 : 16, - bottom: _responsiveUtils.isDesktop(context) ? 25 : 16, - left: _responsiveUtils.isDesktop(context) ? 32 : 16), + padding: EdgeInsetsDirectional.only( + top: responsiveUtils.isDesktop(context) ? 25 : 16, + bottom: responsiveUtils.isDesktop(context) ? 25 : 16, + start: responsiveUtils.isDesktop(context) ? 32 : 16, + ), child: Row(children: [ - (SloganBuilder(arrangedByHorizontal: true) - ..setSloganText(AppLocalizations.of(context).app_name) - ..setSloganTextAlign(TextAlign.center) - ..setSloganTextStyle(const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold)) - ..setSizeLogo(24) - ..setLogo(_imagePaths.icLogoTMail)) - .build(), + SloganBuilder( + sizeLogo: 24, + text: AppLocalizations.of(context).app_name, + textAlign: TextAlign.center, + textStyle: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold), + logoSVG: imagePaths.icTMailLogo + ), Obx(() { if (controller.mailboxDashBoardController.appInformation.value != null) { return _buildVersionInformation(context, controller.mailboxDashBoardController.appInformation.value!); @@ -87,116 +79,130 @@ class MailboxView extends GetWidget ); } - Widget _buildLoadingView() { - return Obx(() => controller.viewState.value.fold( - (failure) => const SizedBox.shrink(), - (success) => success is LoadingState - ? Padding(padding: const EdgeInsets.only(top: 16), child: loadingWidget) - : const SizedBox.shrink())); - } - Widget _buildListMailbox(BuildContext context) { return Stack( children: [ - SingleChildScrollView( - controller: controller.mailboxListScrollController, - key: const PageStorageKey('mailbox_list'), - physics: const ClampingScrollPhysics(), - padding: EdgeInsets.only(right: _responsiveUtils.isDesktop(context) ? 16 : 0), - child: Column(children: [ - Obx(() { - if (controller.isSelectionEnabled() || _responsiveUtils.isDesktop(context)) { - return const SizedBox.shrink(); - } - return _buildUserInformation(context); - }), - _buildLoadingView(), - AppConfig.appGridDashboardAvailable && _responsiveUtils.isWebNotDesktop(context) - ? Column(children: [ - _buildAppGridDashboard(context), - const SizedBox(height: 8), - const Divider(color: AppColor.colorDividerMailbox, height: 0.5, thickness: 0.2), - const SizedBox(height: 8), - ]) - : const SizedBox.shrink(), - Obx(() { - if (controller.defaultMailboxIsNotEmpty) { - return _buildMailboxCategory( - context, - MailboxCategories.exchange, - controller.defaultRootNode - ); - } else { - return const SizedBox.shrink(); - } - }), - const SizedBox(height: 8), - const Divider(color: AppColor.colorDividerMailbox, height: 0.5, thickness: 0.2), - const SizedBox(height: 13), - Padding( - padding: EdgeInsets.only( - left: _responsiveUtils.isDesktop(context) ? 0 : 12, - bottom: 8 - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text(AppLocalizations.of(context).mailBoxes, - style: const TextStyle( - fontSize: 17, - color: Colors.black, - fontWeight: FontWeight.bold)), - Padding( - padding: EdgeInsets.only(right: _responsiveUtils.isDesktop(context) ? 0 : 12), - child: Row( - children: [ - buildIconWeb( - minSize: 40, - iconPadding: EdgeInsets.zero, - icon: SvgPicture.asset( - _imagePaths.icSearchBar, - color: AppColor.colorTextButton, - fit: BoxFit.fill - ), - onTap: () => controller.openSearchViewAction(context) - ), - buildIconWeb( - minSize: 40, - iconSize: 20, - iconPadding: EdgeInsets.zero, - splashRadius: 15, - icon: SvgPicture.asset(_imagePaths.icAddNewFolder, color: AppColor.colorTextButton, fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).new_mailbox, - onTap: () => controller.goToCreateNewMailboxView(context)), - ], + ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + physics: const BouncingScrollPhysics(), + dragDevices: { + PointerDeviceKind.touch, + PointerDeviceKind.mouse, + PointerDeviceKind.trackpad + }, + ), + child: RefreshIndicator( + color: AppColor.primaryColor, + onRefresh: controller.refreshAllMailbox, + child: SingleChildScrollView( + controller: controller.mailboxListScrollController, + key: const PageStorageKey('mailbox_list'), + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsetsDirectional.only(end: responsiveUtils.isDesktop(context) ? 16 : 0), + child: Column(children: [ + if (!responsiveUtils.isDesktop(context)) + Obx(() => UserInformationWidget( + userProfile: controller.mailboxDashBoardController.userProfile.value, + subtitle: AppLocalizations.of(context).manage_account, + onSubtitleClick: controller.mailboxDashBoardController.goToSettings, + border: const Border( + bottom: BorderSide( + color: AppColor.colorDividerHorizontal, + width: 0.5, + ) + ), + )), + Obx(() => MailboxLoadingBarWidget(viewState: controller.viewState.value)), + AppConfig.appGridDashboardAvailable && responsiveUtils.isWebNotDesktop(context) + ? buildAppGridDashboard(context, responsiveUtils, imagePaths, controller) + : const SizedBox.shrink(), + const SizedBox(height: 8), + Obx(() { + if (controller.defaultMailboxIsNotEmpty) { + return _buildMailboxCategory( + context, + MailboxCategories.exchange, + controller.defaultRootNode + ); + } else { + return const SizedBox.shrink(); + } + }), + const SizedBox(height: 8), + const Divider(color: AppColor.colorDividerMailbox, height: 1), + const SizedBox(height: 13), + Padding( + padding: EdgeInsetsDirectional.only( + start: responsiveUtils.isDesktop(context) ? 0 : 12, + bottom: 8 + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded(child: Text( + AppLocalizations.of(context).folders, + style: const TextStyle( + fontSize: 17, + color: Colors.black, + fontWeight: FontWeight.bold + ) )), - ]), + Padding( + padding: EdgeInsetsDirectional.only(end: responsiveUtils.isDesktop(context) ? 0 : 12), + child: Row( + children: [ + buildIconWeb( + minSize: 40, + iconPadding: EdgeInsets.zero, + icon: SvgPicture.asset( + imagePaths.icSearchBar, + colorFilter: AppColor.colorTextButton.asFilter(), + fit: BoxFit.fill + ), + onTap: () => controller.openSearchViewAction(context) + ), + buildIconWeb( + minSize: 40, + iconSize: 20, + iconPadding: EdgeInsets.zero, + splashRadius: 15, + icon: SvgPicture.asset( + imagePaths.icAddNewFolder, + colorFilter: AppColor.colorTextButton.asFilter(), + fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).new_mailbox, + onTap: () => controller.goToCreateNewMailboxView(context)), + ], + )), + ]), + ), + Obx(() { + if (controller.personalMailboxIsNotEmpty) { + return _buildMailboxCategory( + context, + MailboxCategories.personalFolders, + controller.personalRootNode + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.teamMailboxesIsNotEmpty) { + return _buildMailboxCategory( + context, + MailboxCategories.teamMailboxes, + controller.teamMailboxesRootNode + ); + } else { + return const SizedBox.shrink(); + } + }) + ]) ), - Obx(() { - if (controller.personalMailboxIsNotEmpty) { - return _buildMailboxCategory( - context, - MailboxCategories.personalMailboxes, - controller.personalRootNode - ); - } else { - return const SizedBox.shrink(); - } - }), - Obx(() { - if (controller.teamMailboxesIsNotEmpty) { - return _buildMailboxCategory( - context, - MailboxCategories.teamMailboxes, - controller.teamMailboxesRootNode - ); - } else { - return const SizedBox.shrink(); - } - }) - ]) + ), ), - Obx(() => controller.mailboxDashBoardController.isDraggingMailbox + Obx(() => controller.mailboxDashBoardController.isDraggingMailbox && controller.activeScrollTop ? Align( alignment: Alignment.topCenter, child: InkWell( @@ -205,7 +211,7 @@ class MailboxView extends GetWidget child: Container( height: 40))) : const SizedBox.shrink()), - Obx(() => controller.mailboxDashBoardController.isDraggingMailbox + Obx(() => controller.mailboxDashBoardController.isDraggingMailbox && controller.activeScrollBottom ? Align( alignment: Alignment.bottomCenter, child: InkWell( @@ -218,22 +224,6 @@ class MailboxView extends GetWidget ); } - Widget _buildUserInformation(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Column(children: [ - Padding( - padding: const EdgeInsets.only(left: 16, bottom: 10), - child: UserInformationWidgetBuilder( - _imagePaths, - controller.mailboxDashBoardController.userProfile.value, - subtitle: AppLocalizations.of(context).manage_account, - onSubtitleClick: () => controller.mailboxDashBoardController.goToSettings())), - const Divider(color: AppColor.colorDividerMailbox, height: 0.5, thickness: 0.2) - ]), - ); - } - Widget _buildBodyMailboxCategory( BuildContext context, MailboxCategories categories, @@ -243,8 +233,8 @@ class MailboxView extends GetWidget return Container( padding: EdgeInsets.only( - right: _responsiveUtils.isDesktop(context) ? 0 : 16, - left: _responsiveUtils.isDesktop(context) ? 0 : 16), + right: responsiveUtils.isDesktop(context) ? 0 : 16, + left: responsiveUtils.isDesktop(context) ? 0 : 16), child: TreeView( key: Key('${categories.keyValue}_mailbox_list'), children: _buildListChildTileWidget( @@ -265,8 +255,8 @@ class MailboxView extends GetWidget children: [ buildHeaderMailboxCategory( context, - _responsiveUtils, - _imagePaths, + responsiveUtils, + imagePaths, categories, controller, toggleMailboxCategories: controller.toggleMailboxCategories @@ -294,50 +284,47 @@ class MailboxView extends GetWidget context, key: const Key('children_tree_mailbox_child'), isExpanded: mailboxNode.expandMode == ExpandMode.EXPAND, - parent: Obx(() => (MailBoxFolderTileBuilder( - context, - _imagePaths, - mailboxNode, - lastNode: lastNode, - mailboxNodeSelected: controller.mailboxDashBoardController.selectedMailbox.value) - ..addOnClickOpenMailboxNodeAction((mailboxNode) => controller.openMailbox(context, mailboxNode.item)) - ..addOnClickExpandMailboxNodeAction((mailboxNode) => controller.toggleMailboxFolder(mailboxNode)) - ..addOnClickOpenMenuMailboxNodeAction((position, mailboxNode) { + paddingChild: const EdgeInsetsDirectional.only(start: 14), + parent: Obx(() => MailboxItemWidget( + mailboxNode: mailboxNode, + mailboxNodeSelected: controller.mailboxDashBoardController.selectedMailbox.value, + onOpenMailboxFolderClick: (mailboxNode) => controller.openMailbox(context, mailboxNode.item), + onExpandFolderActionClick: (mailboxNode) => controller.toggleMailboxFolder(mailboxNode, controller.mailboxListScrollController), + onSelectMailboxFolderClick: controller.selectMailboxNode, + onDragItemAccepted: _handleDragItemAccepted, + onMenuActionClick: (position, mailboxNode) { openMailboxMenuActionOnWeb( context, - _imagePaths, - _responsiveUtils, + imagePaths, + responsiveUtils, position, mailboxNode.item, controller ); - }) - ..addOnSelectMailboxNodeAction((mailboxNode) => controller.selectMailboxNode(mailboxNode)) - ..addOnDragEmailToMailboxAccepted(_handleDragItemAccepted) - ).build()), + }, + onEmptyMailboxActionCallback: (mailboxNode) => controller.emptyMailboxAction(context, mailboxNode.item), + )), children: _buildListChildTileWidget(context, mailboxNode) ).build(); } else { - return Obx(() => (MailBoxFolderTileBuilder( - context, - _imagePaths, - mailboxNode, - lastNode: lastNode, - mailboxNodeSelected: controller.mailboxDashBoardController.selectedMailbox.value) - ..addOnClickOpenMailboxNodeAction((mailboxNode) => controller.openMailbox(context, mailboxNode.item)) - ..addOnClickOpenMenuMailboxNodeAction((position, mailboxNode) { + return Obx(() => MailboxItemWidget( + mailboxNode: mailboxNode, + mailboxNodeSelected: controller.mailboxDashBoardController.selectedMailbox.value, + onOpenMailboxFolderClick: (mailboxNode) => controller.openMailbox(context, mailboxNode.item), + onSelectMailboxFolderClick: controller.selectMailboxNode, + onDragItemAccepted: _handleDragItemAccepted, + onMenuActionClick: (position, mailboxNode) { openMailboxMenuActionOnWeb( context, - _imagePaths, - _responsiveUtils, + imagePaths, + responsiveUtils, position, mailboxNode.item, controller ); - }) - ..addOnSelectMailboxNodeAction((mailboxNode) => controller.selectMailboxNode(mailboxNode)) - ..addOnDragEmailToMailboxAccepted(_handleDragItemAccepted) - ).build()); + }, + onEmptyMailboxActionCallback: (mailboxNode) => controller.emptyMailboxAction(context, mailboxNode.item), + )); } }).toList() ?? []; } @@ -364,90 +351,4 @@ class MailboxView extends GetWidget ), ); } - - Widget _buildAppGridDashboard(BuildContext context) { - return Column( - children: [ - _buildGoToApplicationsCategory(context, MailboxCategories.appGrid), - AnimatedContainer( - padding: const EdgeInsets.only(top: 8), - duration: const Duration(milliseconds: 400), - child: Obx(() { - return controller.mailboxDashBoardController.appGridDashboardController.appDashboardExpandMode.value == ExpandMode.EXPAND - ? _buildAppGridInMailboxView(context) - : const Offstage(); - }) - ) - ]); - } - - Widget _buildGoToApplicationsCategory(BuildContext context, MailboxCategories categories) { - return Padding( - padding: EdgeInsets.only( - top: 8, - left: _responsiveUtils.isDesktop(context) ? 0 : 36, - right: _responsiveUtils.isDesktop(context) ? 0 : 28 - ), - child: Row( - children: [ - buildIconWeb( - splashRadius: 5, - iconPadding: EdgeInsets.zero, - minSize: 12, - iconSize: 28, - icon: SvgPicture.asset( - _imagePaths.icAppDashboard, - color: AppColor.primaryColor, - fit: BoxFit.fill - ), - tooltip: AppLocalizations.of(context).appGridTittle), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 12), - child: Text(categories.getTitle(context), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 16, - color: AppColor.colorTextButton, - fontWeight: FontWeight.w500 - ) - ) - ) - ), - buildIconWeb( - splashRadius: 5, - iconPadding: EdgeInsets.zero, - minSize: 12, - iconSize: 28, - icon: Obx(() => SvgPicture.asset( - controller.mailboxDashBoardController.appGridDashboardController.appDashboardExpandMode.value == ExpandMode.COLLAPSE - ? _imagePaths.icCollapseFolder - : _imagePaths.icExpandFolder, - color: AppColor.primaryColor, - fit: BoxFit.fill - )), - tooltip: AppLocalizations.of(context).appGridTittle, - onTap: () => controller.toggleMailboxCategories(categories) - ), - ] - ) - ); - } - - Widget _buildAppGridInMailboxView(BuildContext context) { - return Obx(() { - final linagoraApps = controller.mailboxDashBoardController.appGridDashboardController.linagoraApplications.value; - if (linagoraApps != null && linagoraApps.apps.isNotEmpty) { - return ListView.builder( - shrinkWrap: true, - itemCount: linagoraApps.apps.length, - itemBuilder: (context, index) { - return AppListDashboardItem(linagoraApps.apps[index]); - } - ); - } - return const SizedBox.shrink(); - }); - } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart b/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart index 36b048962f..291da5eec6 100644 --- a/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart +++ b/lib/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart @@ -4,10 +4,12 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; @@ -17,42 +19,95 @@ import 'package:tmail_ui_user/features/mailbox/presentation/model/context_item_m import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_bottom_sheet_action_tile_builder.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_list_dashboard_item.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; mixin MailboxWidgetMixin { - MailboxActions _mailboxActionForSpam(MailboxDashBoardController dashBoardController) { - return dashBoardController.enableSpamReport + MailboxActions _mailboxActionForSpam(bool spamReportEnabled) { + return spamReportEnabled ? MailboxActions.disableSpamReport : MailboxActions.enableSpamReport; } - List listActionForMailbox( + List _listActionForDefaultMailbox( PresentationMailbox mailbox, - MailboxDashBoardController dashBoardController + bool spamReportEnabled ) { + return [ - if (BuildUtils.isWeb) + if (PlatformInfo.isWeb) MailboxActions.openInNewTab, + MailboxActions.newSubfolder, if (mailbox.isSpam) - _mailboxActionForSpam(dashBoardController), - MailboxActions.markAsRead, + _mailboxActionForSpam(spamReportEnabled), + if (mailbox.countUnReadEmailsAsString.isNotEmpty) + MailboxActions.markAsRead, + if (mailbox.isTrash) + MailboxActions.emptyTrash, + if (PlatformInfo.isWeb && mailbox.isSpam) + MailboxActions.emptySpam, + ]; + } + + List _listActionForPersonalMailbox(PresentationMailbox mailbox) { + return [ + if (PlatformInfo.isWeb && mailbox.isSubscribedMailbox) + MailboxActions.openInNewTab, + MailboxActions.newSubfolder, + if (mailbox.countUnReadEmailsAsString.isNotEmpty) + MailboxActions.markAsRead, MailboxActions.move, MailboxActions.rename, MailboxActions.delete, - if (mailbox.isSupportedDisableMailbox) + if (mailbox.isSubscribedMailbox) MailboxActions.disableMailbox + else + MailboxActions.enableMailbox ]; } + List _listActionForTeamMailbox(PresentationMailbox mailbox) { + return [ + if (PlatformInfo.isWeb && mailbox.isSubscribedMailbox) + MailboxActions.openInNewTab, + if (mailbox.countUnReadEmailsAsString.isNotEmpty) + MailboxActions.markAsRead, + if (mailbox.isTeamMailboxes) + if (mailbox.isSubscribedMailbox) + MailboxActions.disableMailbox + else + MailboxActions.enableMailbox + ]; + } + + List _listActionForAllMailboxType( + PresentationMailbox mailbox, + bool spamReportEnabled + ) { + if (mailbox.isDefault) { + return _listActionForDefaultMailbox(mailbox, spamReportEnabled); + } else if (mailbox.isPersonal) { + return _listActionForPersonalMailbox(mailbox); + } else { + return _listActionForTeamMailbox(mailbox); + } + } + void openMailboxMenuActionOnMobile( BuildContext context, ImagePaths imagePaths, PresentationMailbox mailbox, MailboxController controller ) { - final contextMenuActions = listContextMenuItemAction(mailbox, controller.mailboxDashBoardController); + final contextMenuActions = listContextMenuItemAction( + mailbox, + controller.mailboxDashBoardController.enableSpamReport + ); + + if (contextMenuActions.isEmpty) { + return; + } controller.openContextMenuAction( context, @@ -99,7 +154,7 @@ mixin MailboxWidgetMixin { Key('${contextMenuItem.action.name}_action'), SvgPicture.asset( contextMenuItem.action.getContextMenuIcon(imagePaths), - color: contextMenuItem.action.getColorContextMenuIcon(), + colorFilter: contextMenuItem.action.getColorContextMenuIcon().asFilter(), width: 24, height: 24 ), @@ -118,15 +173,9 @@ mixin MailboxWidgetMixin { List listContextMenuItemAction( PresentationMailbox mailbox, - MailboxDashBoardController dashBoardController, - { - List? mailboxActions - } + bool spamReportEnabled ) { - final mailboxActionsSupported = mailboxActions ?? listActionForMailbox( - mailbox, - dashBoardController - ); + final mailboxActionsSupported = _listActionForAllMailboxType(mailbox, spamReportEnabled); final listContextMenuItemAction = mailboxActionsSupported .map((action) => ContextMenuItemMailboxAction(action, action.getContextMenuItemState(mailbox))) @@ -143,7 +192,14 @@ mixin MailboxWidgetMixin { PresentationMailbox mailbox, MailboxController controller ) { - final contextMenuActions = listContextMenuItemAction(mailbox, controller.mailboxDashBoardController); + final contextMenuActions = listContextMenuItemAction( + mailbox, + controller.mailboxDashBoardController.enableSpamReport + ); + + if (contextMenuActions.isEmpty) { + return; + } if (responsiveUtils.isScreenWithShortestSide(context)) { controller.openContextMenuAction( @@ -210,6 +266,7 @@ mixin MailboxWidgetMixin { contextMenuItem.action.getContextMenuIcon(imagePaths), contextMenuItem.action.getTitleContextMenu(context), colorIcon: contextMenuItem.action.getColorContextMenuIcon(), + padding: const EdgeInsetsDirectional.only(start: 12), iconSize: 24, styleName: TextStyle( fontWeight: FontWeight.w500, @@ -244,14 +301,14 @@ mixin MailboxWidgetMixin { Obx(() { final expandMode = categories.getExpandMode(baseMailboxController.mailboxCategoriesExpandMode.value); return buildIconWeb( - splashRadius: 5, + splashRadius: 12, iconPadding: EdgeInsets.zero, minSize: 12, icon: SvgPicture.asset( expandMode == ExpandMode.EXPAND - ? imagePaths.icExpandFolder - : imagePaths.icCollapseFolder, - color: AppColor.primaryColor, + ? imagePaths.icArrowBottom + : DirectionUtils.isDirectionRTLByLanguage(context) ? imagePaths.icArrowLeft : imagePaths.icArrowRight, + colorFilter: AppColor.primaryColor.asFilter(), fit: BoxFit.fill ), tooltip: expandMode == ExpandMode.EXPAND @@ -274,4 +331,93 @@ mixin MailboxWidgetMixin { ]) ); } + + Widget buildAppGridDashboard( + BuildContext context, + ResponsiveUtils responsiveUtils, + ImagePaths imagePaths, + MailboxController controller + ) { + return Column(children: [ + _buildGoToApplicationsCategory( + context, + responsiveUtils, + imagePaths, + MailboxCategories.appGrid, + controller), + AnimatedContainer( + duration: const Duration(milliseconds: 400), + child: Obx(() { + return controller.mailboxDashBoardController.appGridDashboardController.appDashboardExpandMode.value == ExpandMode.EXPAND + ? _buildAppGridInMailboxView(context, controller) + : const Offstage(); + }) + ), + const Divider(color: AppColor.colorDividerMailbox, height: 1) + ]); + } + + Widget _buildGoToApplicationsCategory( + BuildContext context, + ResponsiveUtils responsiveUtils, + ImagePaths imagePaths, + MailboxCategories categories, + MailboxController controller + ) { + return Padding( + padding: const EdgeInsetsDirectional.only(start: 32, end: 4), + child: Row(children: [ + SvgPicture.asset( + imagePaths.icAppDashboard, + colorFilter: AppColor.primaryColor.asFilter(), + width: 20, + height: 20, + fit: BoxFit.fill), + Expanded(child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text(categories.getTitle(context), + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: const TextStyle( + fontSize: 16, + color: AppColor.colorTextButton, + fontWeight: FontWeight.w500 + ) + ) + )), + buildIconWeb( + icon: Obx(() => SvgPicture.asset( + controller.mailboxDashBoardController.appGridDashboardController.appDashboardExpandMode.value == ExpandMode.COLLAPSE + ? DirectionUtils.isDirectionRTLByLanguage(context) ? imagePaths.icBack : imagePaths.icCollapseFolder + : imagePaths.icExpandFolder, + colorFilter: controller.mailboxDashBoardController.appGridDashboardController.appDashboardExpandMode.value == ExpandMode.COLLAPSE + ? AppColor.colorIconUnSubscribedMailbox.asFilter() + : AppColor.primaryColor.asFilter(), + fit: BoxFit.fill + )), + tooltip: AppLocalizations.of(context).appGridTittle, + onTap: () => controller.toggleMailboxCategories(categories) + ) + ]) + ); + } + + Widget _buildAppGridInMailboxView(BuildContext context, MailboxController controller) { + return Obx(() { + final linagoraApps = controller.mailboxDashBoardController.appGridDashboardController.linagoraApplications.value; + if (linagoraApps != null && linagoraApps.apps.isNotEmpty) { + return ListView.builder( + shrinkWrap: true, + primary: false, + padding: const EdgeInsetsDirectional.only(start: 16, end: 16, bottom: 8), + itemCount: linagoraApps.apps.length, + itemBuilder: (context, index) { + return AppListDashboardItem(linagoraApps.apps[index]); + } + ); + } + return const SizedBox.shrink(); + }); + } } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/model/mailbox_actions.dart b/lib/features/mailbox/presentation/model/mailbox_actions.dart index 0ca8334613..f52c889665 100644 --- a/lib/features/mailbox/presentation/model/mailbox_actions.dart +++ b/lib/features/mailbox/presentation/model/mailbox_actions.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/widget/context_menu_item_action.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -18,7 +19,10 @@ enum MailboxActions { disableSpamReport, enableSpamReport, disableMailbox, - enableMailbox + enableMailbox, + emptyTrash, + emptySpam, + newSubfolder; } extension MailboxActionsExtension on MailboxActions { @@ -42,6 +46,8 @@ extension MailboxActionsExtension on MailboxActions { switch(this) { case MailboxActions.openInNewTab: return AppLocalizations.of(context).openInNewTab; + case MailboxActions.newSubfolder: + return AppLocalizations.of(context).newSubfolder; case MailboxActions.disableSpamReport: return AppLocalizations.of(context).disableSpamReport; case MailboxActions.enableSpamReport: @@ -58,6 +64,10 @@ extension MailboxActionsExtension on MailboxActions { return AppLocalizations.of(context).hideMailBoxes; case MailboxActions.enableMailbox: return AppLocalizations.of(context).showMailbox; + case MailboxActions.emptyTrash: + return AppLocalizations.of(context).emptyTrash; + case MailboxActions.emptySpam: + return AppLocalizations.of(context).deleteAllSpamEmails; default: return ''; } @@ -67,6 +77,8 @@ extension MailboxActionsExtension on MailboxActions { switch(this) { case MailboxActions.openInNewTab: return imagePaths.icOpenInNewTab; + case MailboxActions.newSubfolder: + return imagePaths.icAddNewFolder; case MailboxActions.disableSpamReport: return imagePaths.icSpamReportDisable; case MailboxActions.enableSpamReport: @@ -83,6 +95,10 @@ extension MailboxActionsExtension on MailboxActions { return imagePaths.icHideMailbox; case MailboxActions.enableMailbox: return imagePaths.icShowMailbox; + case MailboxActions.emptyTrash: + return imagePaths.icMailboxTrash; + case MailboxActions.emptySpam: + return imagePaths.icMailboxTrash; default: return ''; } @@ -145,24 +161,19 @@ extension MailboxActionsExtension on MailboxActions { ContextMenuItemState getContextMenuItemState(PresentationMailbox mailbox) { switch(this) { case MailboxActions.openInNewTab: + case MailboxActions.newSubfolder: case MailboxActions.disableSpamReport: case MailboxActions.enableSpamReport: - return mailbox.isPersonal - ? ContextMenuItemState.activated - : ContextMenuItemState.deactivated; case MailboxActions.enableMailbox: case MailboxActions.disableMailbox: - return mailbox.hasRole() - ? ContextMenuItemState.deactivated - : ContextMenuItemState.activated; - case MailboxActions.markAsRead: - return mailbox.getCountUnReadEmails().isNotEmpty - ? ContextMenuItemState.activated - : ContextMenuItemState.deactivated; case MailboxActions.move: case MailboxActions.rename: case MailboxActions.delete: - return !mailbox.hasRole() && mailbox.isPersonal + case MailboxActions.emptyTrash: + case MailboxActions.emptySpam: + return ContextMenuItemState.activated; + case MailboxActions.markAsRead: + return mailbox.countUnReadEmailsAsString.isNotEmpty ? ContextMenuItemState.activated : ContextMenuItemState.deactivated; default: diff --git a/lib/features/mailbox/presentation/model/mailbox_categories.dart b/lib/features/mailbox/presentation/model/mailbox_categories.dart index a4b2f1bbba..0c05ec080f 100644 --- a/lib/features/mailbox/presentation/model/mailbox_categories.dart +++ b/lib/features/mailbox/presentation/model/mailbox_categories.dart @@ -6,7 +6,7 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; enum MailboxCategories { exchange, - personalMailboxes, + personalFolders, appGrid, teamMailboxes } @@ -17,8 +17,8 @@ extension MailboxCategoriessExtension on MailboxCategories { switch(this) { case MailboxCategories.exchange: return 'exchange'; - case MailboxCategories.personalMailboxes: - return 'personalMailboxes'; + case MailboxCategories.personalFolders: + return 'personalFolders'; case MailboxCategories.appGrid: return 'appGrid'; case MailboxCategories.teamMailboxes: @@ -30,8 +30,8 @@ extension MailboxCategoriessExtension on MailboxCategories { switch(this) { case MailboxCategories.exchange: return AppLocalizations.of(context).exchange; - case MailboxCategories.personalMailboxes: - return AppLocalizations.of(context).personalMailboxes; + case MailboxCategories.personalFolders: + return AppLocalizations.of(context).personalFolders; case MailboxCategories.appGrid: return AppLocalizations.of(context).appGridTittle; case MailboxCategories.teamMailboxes: @@ -43,8 +43,8 @@ extension MailboxCategoriessExtension on MailboxCategories { switch(this) { case MailboxCategories.exchange: return categoriesExpandMode.defaultMailbox; - case MailboxCategories.personalMailboxes: - return categoriesExpandMode.personalMailboxes; + case MailboxCategories.personalFolders: + return categoriesExpandMode.personalFolders; case MailboxCategories.teamMailboxes: return categoriesExpandMode.teamMailboxes; default: diff --git a/lib/features/mailbox/presentation/model/mailbox_categories_expand_mode.dart b/lib/features/mailbox/presentation/model/mailbox_categories_expand_mode.dart index 6f46d64c47..e0f32c2aa6 100644 --- a/lib/features/mailbox/presentation/model/mailbox_categories_expand_mode.dart +++ b/lib/features/mailbox/presentation/model/mailbox_categories_expand_mode.dart @@ -4,21 +4,21 @@ import 'package:model/mailbox/expand_mode.dart'; class MailboxCategoriesExpandMode with EquatableMixin { ExpandMode defaultMailbox; - ExpandMode personalMailboxes; + ExpandMode personalFolders; ExpandMode teamMailboxes; MailboxCategoriesExpandMode({ required this.defaultMailbox, - required this.personalMailboxes, + required this.personalFolders, required this.teamMailboxes}); factory MailboxCategoriesExpandMode.initial() { return MailboxCategoriesExpandMode( - defaultMailbox: ExpandMode.EXPAND, - personalMailboxes: ExpandMode.EXPAND, + defaultMailbox: ExpandMode.EXPAND, + personalFolders: ExpandMode.EXPAND, teamMailboxes: ExpandMode.EXPAND); } @override - List get props => [defaultMailbox, personalMailboxes, teamMailboxes]; + List get props => [defaultMailbox, personalFolders, teamMailboxes]; } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/model/mailbox_node.dart b/lib/features/mailbox/presentation/model/mailbox_node.dart index d454e4e247..59250cf4cc 100644 --- a/lib/features/mailbox/presentation/model/mailbox_node.dart +++ b/lib/features/mailbox/presentation/model/mailbox_node.dart @@ -1,12 +1,13 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:model/mailbox/mailbox_state.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; -class MailboxNode with EquatableMixin{ +class MailboxNode with EquatableMixin { static final PresentationMailbox _root = PresentationMailbox(MailboxId(Id('root'))); PresentationMailbox item; @@ -21,6 +22,8 @@ class MailboxNode with EquatableMixin{ bool hasChildren() => childrenItems?.isNotEmpty ?? false; + bool hasParents() => item.hasParentId(); + bool get isActivated => nodeState == MailboxState.activated; bool get isSelected => selectMode == SelectMode.ACTIVE; @@ -53,8 +56,8 @@ class MailboxNode with EquatableMixin{ } List? updateNode(MailboxId mailboxId, MailboxNode newNode, {MailboxNode? parent}) { - List? _children = parent == null ? childrenItems : parent.childrenItems; - return _children?.map((MailboxNode child) { + List? children = parent == null ? childrenItems : parent.childrenItems; + return children?.map((MailboxNode child) { if (child.item.id == mailboxId) { return newNode; } else { @@ -91,8 +94,8 @@ class MailboxNode with EquatableMixin{ } List? toggleSelectNode(MailboxNode selectedMailboxMode, {MailboxNode? parent}) { - List? _children = parent == null ? childrenItems : parent.childrenItems; - return _children?.map((MailboxNode child) { + List? children = parent == null ? childrenItems : parent.childrenItems; + return children?.map((MailboxNode child) { if (child.item.id == selectedMailboxMode.item.id) { return child.toggleSelectMailboxNode(); } else { @@ -105,8 +108,8 @@ class MailboxNode with EquatableMixin{ } List? toSelectedNode({required SelectMode selectMode, ExpandMode? newExpandMode, MailboxNode? parent}) { - List? _children = parent == null ? childrenItems : parent.childrenItems; - return _children?.map((MailboxNode child) { + List? children = parent == null ? childrenItems : parent.childrenItems; + return children?.map((MailboxNode child) { if (child.hasChildren()) { return child.copyWith( children: toSelectedNode(selectMode: selectMode, newExpandMode: newExpandMode, parent: child), diff --git a/lib/features/mailbox/presentation/model/mailbox_tree.dart b/lib/features/mailbox/presentation/model/mailbox_tree.dart index 0e83f641b7..7cdcd1d8a0 100644 --- a/lib/features/mailbox/presentation/model/mailbox_tree.dart +++ b/lib/features/mailbox/presentation/model/mailbox_tree.dart @@ -6,6 +6,8 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'mailbox_node.dart'; @@ -85,7 +87,12 @@ class MailboxTree with EquatableMixin { if (matchedNode == null) { return null; } - String path = '${matchedNode.item.name?.name}'; + String path = ''; + if (currentContext != null) { + path = matchedNode.item.getDisplayName(currentContext!); + } else { + path = '${matchedNode.item.name?.name}'; + } var parentId = matchedNode.item.parentId; @@ -94,12 +101,30 @@ class MailboxTree with EquatableMixin { if (parentNode == null) { break; } - path = '${parentNode.item.name?.name}/$path'; + if (currentContext != null) { + path = '${parentNode.item.getDisplayName(currentContext!)}/$path'; + } else { + path = '${parentNode.item.name?.name}/$path'; + } parentId = parentNode.item.parentId; } return path; } + List? getAncestorList(MailboxNode mailboxNode) { + var parentId = mailboxNode.item.parentId; + List ancestor = []; + while(parentId != null) { + final parentNode = findNode((node) => node.item.id == parentId); + if (parentNode == null) { + break; + } + ancestor.add(parentNode); + parentId = parentNode.item.parentId; + } + return ancestor.isNotEmpty ? ancestor : null; + } + Map get mapPresentationMailboxByRole { if (root.childrenItems?.isEmpty == true) { return {}; diff --git a/lib/features/mailbox/presentation/styles/count_of_emails_styles.dart b/lib/features/mailbox/presentation/styles/count_of_emails_styles.dart new file mode 100644 index 0000000000..494f62acfc --- /dev/null +++ b/lib/features/mailbox/presentation/styles/count_of_emails_styles.dart @@ -0,0 +1,8 @@ + +import 'package:flutter/material.dart'; + +class CountOfEmailsStyles { + static const Color textColor = Colors.black; + static const double textSize = 13; + static const FontWeight textFontWeight = FontWeight.w400; +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/styles/empty_mailbox_dialog_overlay_styles.dart b/lib/features/mailbox/presentation/styles/empty_mailbox_dialog_overlay_styles.dart new file mode 100644 index 0000000000..3c87da31b9 --- /dev/null +++ b/lib/features/mailbox/presentation/styles/empty_mailbox_dialog_overlay_styles.dart @@ -0,0 +1,46 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EmptyMailboxDialogOverlayStyles { + static const double width = 300; + static const double radius = 16; + static const double closeButtonIconSize = 28; + static const double space = 12; + static const double buttonSpace = 9; + static const double buttonRadius = 8; + static const double elevation = 8; + + static const EdgeInsetsGeometry margin = EdgeInsetsDirectional.only(start: 12); + static const EdgeInsetsGeometry padding = EdgeInsets.all(12); + static const EdgeInsetsGeometry iconPadding = EdgeInsets.zero; + static const EdgeInsetsGeometry buttonPadding = EdgeInsets.symmetric(vertical: 8, horizontal: 12); + static const EdgeInsetsGeometry emptyButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 2, horizontal: 5); + + static const Color backgroundColor = Colors.white; + static const Color closeButtonColor = AppColor.colorClosePopupDialogButton; + static const Color cancelButtonColor = AppColor.colorCancelPopupDialogButton; + static const Color emptyButtonColor = AppColor.colorEmptyPopupDialogButton; + static const Color emptyButtonBackground = Colors.transparent; + static Color get shadowColor => Colors.grey.shade500; + + static const TextStyle titleTextStyle = TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + color: Colors.black + ); + static const TextStyle messageTextStyle = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Colors.black + ); + static const TextStyle buttonTextStyle = TextStyle( + fontSize: 13, + fontWeight: FontWeight.w400, + color: Colors.black + ); + static const TextStyle emptyButtonTextStyle = TextStyle( + fontWeight: FontWeight.w400, + fontSize: 15, + color: AppColor.colorTextBody + ); +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/styles/empty_mailbox_popup_dialog_widget_styles.dart b/lib/features/mailbox/presentation/styles/empty_mailbox_popup_dialog_widget_styles.dart new file mode 100644 index 0000000000..627860aece --- /dev/null +++ b/lib/features/mailbox/presentation/styles/empty_mailbox_popup_dialog_widget_styles.dart @@ -0,0 +1,16 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EmptyMailboxPopupDialogWidgetStyles { + static const EdgeInsetsGeometry emptyButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 4, horizontal: 10); + + static const Color emptyButtonBackground = Colors.transparent; + + static const TextStyle emptyButtonTextStyle = TextStyle( + fontWeight: FontWeight.w400, + fontSize: 15, + color: AppColor.colorTextBody + ); + + static const Offset dialogOverlayOffset = Offset(0.0, 50.0); +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/styles/label_mailbox_item_widget_styles.dart b/lib/features/mailbox/presentation/styles/label_mailbox_item_widget_styles.dart new file mode 100644 index 0000000000..6f3741d2c6 --- /dev/null +++ b/lib/features/mailbox/presentation/styles/label_mailbox_item_widget_styles.dart @@ -0,0 +1,17 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class LabelMailboxItemWidgetStyles { + static const double labelTeamMailboxTextSize = 16; + static const double labelFolderTextSize = 15; + static const double teamMailboxTextSize = 15; + + static const Color labelTeamMailboxTextColor = Colors.black; + static const Color labelFolderTextColor = AppColor.colorNameEmail; + static const Color teamMailboxTextColor = AppColor.colorEmailAddressFull; + + static const FontWeight labelTeamMailboxTextFontWeight = FontWeight.bold; + static const FontWeight labelFolderTextFontWeight = FontWeight.normal; + static const FontWeight teamMailboxTextFontWeight = FontWeight.w400; +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/styles/leading_mailbox_item_widget_styles.dart b/lib/features/mailbox/presentation/styles/leading_mailbox_item_widget_styles.dart new file mode 100644 index 0000000000..22563ce174 --- /dev/null +++ b/lib/features/mailbox/presentation/styles/leading_mailbox_item_widget_styles.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:flutter/cupertino.dart'; + +class LeadingMailboxItemWidgetStyles { + static const double expandIconSize = 15; + static const double expandIconSplashRadius = 12; + static const double emptyBoxSize = 32; + + static const Color displayColor = AppColor.primaryColor; + static const Color normalColor = AppColor.colorIconUnSubscribedMailbox; + + static const EdgeInsetsGeometry expandIconPadding = EdgeInsetsDirectional.only(start: 8); + + static Matrix4 mailboxIconTransform(BuildContext context) => Matrix4.translationValues( + DirectionUtils.isDirectionRTLByLanguage(context) ? 0.0 : -4.0, + 0.0, + 0.0 + ); +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/styles/mailbox_icon_widget_styles.dart b/lib/features/mailbox/presentation/styles/mailbox_icon_widget_styles.dart new file mode 100644 index 0000000000..58592e1ab7 --- /dev/null +++ b/lib/features/mailbox/presentation/styles/mailbox_icon_widget_styles.dart @@ -0,0 +1,6 @@ + +import 'package:core/utils/platform_info.dart'; + +class MailboxIconWidgetStyles { + static const double iconSize = PlatformInfo.isWeb ? 24 : 20; +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/styles/mailbox_item_widget_styles.dart b/lib/features/mailbox/presentation/styles/mailbox_item_widget_styles.dart new file mode 100644 index 0000000000..ca551b0bf9 --- /dev/null +++ b/lib/features/mailbox/presentation/styles/mailbox_item_widget_styles.dart @@ -0,0 +1,7 @@ + +class MailboxItemWidgetStyles { + static const double selectionIconSize = 20; + static const double space = 4; + static const double padding = 8; + static const double borderRadius = 8; +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/styles/mailbox_loading_bar_widget_styles.dart b/lib/features/mailbox/presentation/styles/mailbox_loading_bar_widget_styles.dart new file mode 100644 index 0000000000..0dfb2d2719 --- /dev/null +++ b/lib/features/mailbox/presentation/styles/mailbox_loading_bar_widget_styles.dart @@ -0,0 +1,6 @@ + +import 'package:flutter/material.dart'; + +class MailboxLoadingBarWidgetStyles { + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.only(top: 16); +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/styles/trailing_mailbox_item_widget_styles.dart b/lib/features/mailbox/presentation/styles/trailing_mailbox_item_widget_styles.dart new file mode 100644 index 0000000000..8b88daa18f --- /dev/null +++ b/lib/features/mailbox/presentation/styles/trailing_mailbox_item_widget_styles.dart @@ -0,0 +1,12 @@ + +import 'package:flutter/material.dart'; + +class TrailingMailboxItemWidgetStyles { + static const double menuIconSize = 20; + + static const Color menuIconBackgroundColor = Colors.transparent; + + static const EdgeInsetsGeometry countEmailsPadding = EdgeInsetsDirectional.only(start: 5); + static const EdgeInsetsGeometry menuIconMargin = EdgeInsetsDirectional.only(start: 5); + static const EdgeInsetsGeometry menuIconPadding = EdgeInsetsDirectional.all(2); +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/utils/mailbox_method_action_define.dart b/lib/features/mailbox/presentation/utils/mailbox_method_action_define.dart index d2d36835a3..035c1f0771 100644 --- a/lib/features/mailbox/presentation/utils/mailbox_method_action_define.dart +++ b/lib/features/mailbox/presentation/utils/mailbox_method_action_define.dart @@ -15,4 +15,5 @@ typedef OnClickOpenMailboxNodeAction = void Function(MailboxNode); typedef OnSelectMailboxNodeAction = void Function(MailboxNode); typedef OnClickOpenMenuMailboxNodeAction = void Function(RelativeRect, MailboxNode); typedef OnLongPressMailboxNodeAction = void Function(MailboxNode); -typedef OnClickSubscribeMailboxAction = void Function(MailboxNode); \ No newline at end of file +typedef OnClickSubscribeMailboxAction = void Function(MailboxNode); +typedef OnEmptyMailboxActionCallback = void Function(MailboxNode); \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/bottom_bar_selection_mailbox_widget.dart b/lib/features/mailbox/presentation/widgets/bottom_bar_selection_mailbox_widget.dart index 3ca8a43ffb..99296995b6 100644 --- a/lib/features/mailbox/presentation/widgets/bottom_bar_selection_mailbox_widget.dart +++ b/lib/features/mailbox/presentation/widgets/bottom_bar_selection_mailbox_widget.dart @@ -1,133 +1,62 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:flutter/material.dart'; -import 'package:model/model.dart'; +import 'package:get/get.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnMailboxActionsClick = void Function(MailboxActions, List); -class BottomBarSelectionMailboxWidget { +class BottomBarSelectionMailboxWidget extends StatelessWidget { - final BuildContext _context; - final ImagePaths _imagePaths; final List _listSelectionMailbox; + final List _listMailboxActions; + final OnMailboxActionsClick onMailboxActionsClick; - OnMailboxActionsClick? _onMailboxActionsClick; - - BottomBarSelectionMailboxWidget( - this._context, - this._imagePaths, + const BottomBarSelectionMailboxWidget( this._listSelectionMailbox, - ); + this._listMailboxActions, + { + Key? key, + required this.onMailboxActionsClick + } + ) : super(key: key); - void addOnMailboxActionsClick(OnMailboxActionsClick onMailboxActionsClick) { - _onMailboxActionsClick = onMailboxActionsClick; - } - Widget build() { - return Container( - key: const Key('bottom_bar_selection_mailbox_widget'), - alignment: Alignment.center, - color: Colors.white, - child: MediaQuery( - data: const MediaQueryData(padding: EdgeInsets.zero), - child: SafeArea(child: _buildListOptionButton()) - ) - ); - } + @override + Widget build(BuildContext context) { + final responsiveUtils = Get.find(); + final imagePaths = Get.find(); - Widget _buildListOptionButton() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded(child: (ButtonBuilder(_imagePaths.icMoveMailbox) - ..key(const Key('button_move_all_mailbox')) - ..paddingIcon(const EdgeInsets.all(8)) - ..textStyle(TextStyle( - fontSize: 12, - color: _isMoveMailboxValid - ? AppColor.colorTextButton - : AppColor.colorTextButton.withOpacity(0.3))) - ..iconColor(_isMoveMailboxValid - ? AppColor.colorTextButton - : AppColor.colorTextButton.withOpacity(0.3)) - ..onPressActionClick(() { - if (_isMoveMailboxValid) { - _onMailboxActionsClick?.call(MailboxActions.move, _listSelectionMailbox); - } - }) - ..text(AppLocalizations.of(_context).move, isVertical: true)) - .build()), - Expanded(child: (ButtonBuilder(_imagePaths.icRenameMailbox) - ..key(const Key('button_rename_mailbox')) - ..paddingIcon(const EdgeInsets.all(8)) - ..textStyle(TextStyle( - fontSize: 12, - color: _isRenameMailboxValid - ? AppColor.colorTextButton - : AppColor.colorTextButton.withOpacity(0.3))) - ..iconColor(_isRenameMailboxValid - ? AppColor.colorTextButton - : AppColor.colorTextButton.withOpacity(0.3)) - ..onPressActionClick(() { - if (_isRenameMailboxValid) { - _onMailboxActionsClick?.call(MailboxActions.rename, _listSelectionMailbox); - } - }) - ..text(AppLocalizations.of(_context).rename, isVertical: true)) - .build()), - Expanded(child: (ButtonBuilder(_imagePaths.icMarkAsRead) - ..key(const Key('button_mark_read_all_mailbox')) - ..paddingIcon(const EdgeInsets.all(8)) - ..textStyle(TextStyle( - fontSize: 12, - color: _isMarkAsReadMailboxValid - ? AppColor.colorTextButton - : AppColor.colorTextButton.withOpacity(0.3))) - ..iconColor(_isMarkAsReadMailboxValid - ? AppColor.colorTextButton - : AppColor.colorTextButton.withOpacity(0.3)) - ..onPressActionClick(() { - if (_isMarkAsReadMailboxValid) { - _onMailboxActionsClick?.call(MailboxActions.markAsRead, _listSelectionMailbox); - } - }) - ..text(AppLocalizations.of(_context).mark_as_read, isVertical: true)) - .build()), - Expanded(child: (ButtonBuilder(_imagePaths.icDeleteMailbox) - ..key(const Key('button_delete_all_mailbox')) - ..paddingIcon(const EdgeInsets.all(8)) - ..textStyle(TextStyle( - fontSize: 12, - color: _isDeleteMailboxValid - ? AppColor.colorTextButton - : AppColor.colorTextButton.withOpacity(0.3))) - ..iconColor(_isDeleteMailboxValid - ? AppColor.colorTextButton - : AppColor.colorTextButton.withOpacity(0.3)) - ..onPressActionClick(() { - if (_isDeleteMailboxValid) { - _onMailboxActionsClick?.call(MailboxActions.delete, _listSelectionMailbox); - } - }) - ..text(AppLocalizations.of(_context).delete, isVertical: true)) - .build()) - ] + return Container( + decoration: const BoxDecoration( + border: Border(top: BorderSide( + color: AppColor.colorDividerHorizontal, + width: 0.5, + )), + ), + child: IntrinsicHeight( + child: Row(children: _listMailboxActions + .map((action) { + return Expanded(child: TMailButtonWidget( + key: Key('${action.name}_button'), + text: responsiveUtils.isLandscapeMobile(context) + ? '' + : action.getTitleContextMenu(context), + icon: action.getContextMenuIcon(imagePaths), + borderRadius: 0, + backgroundColor: Colors.transparent, + flexibleText: true, + tooltipMessage: action.getTitleContextMenu(context), + textStyle: const TextStyle(fontSize: 12, color: AppColor.colorTextButton), + onTapActionCallback: () => onMailboxActionsClick.call(action, _listSelectionMailbox), + )); + }) + .toList() + ), + ), ); } - - bool get _isDeleteMailboxValid => _isAllFolderMailbox; - - bool get _isRenameMailboxValid => _listSelectionMailbox.length == 1 - && _isAllFolderMailbox; - - bool get _isMarkAsReadMailboxValid => _listSelectionMailbox.length == 1 - && _listSelectionMailbox.first.getCountUnReadEmails().isNotEmpty; - - bool get _isMoveMailboxValid => _listSelectionMailbox.length == 1 - && _isAllFolderMailbox; - - bool get _isAllFolderMailbox => - _listSelectionMailbox.every((mailbox) => !mailbox.hasRole()); } \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/count_of_emails_widget.dart b/lib/features/mailbox/presentation/widgets/count_of_emails_widget.dart new file mode 100644 index 0000000000..edf0f1dcb5 --- /dev/null +++ b/lib/features/mailbox/presentation/widgets/count_of_emails_widget.dart @@ -0,0 +1,26 @@ + +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/styles/count_of_emails_styles.dart'; + +class CountOfEmailsWidget extends StatelessWidget { + + final String value; + + const CountOfEmailsWidget({super.key, required this.value}); + + @override + Widget build(BuildContext context) { + return Text( + value, + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: const TextStyle( + fontSize: CountOfEmailsStyles.textSize, + color: CountOfEmailsStyles.textColor, + fontWeight: CountOfEmailsStyles.textFontWeight + ), + ); + } +} diff --git a/lib/features/mailbox/presentation/widgets/empty_mailbox_dialog_overlay.dart b/lib/features/mailbox/presentation/widgets/empty_mailbox_dialog_overlay.dart new file mode 100644 index 0000000000..21c437db55 --- /dev/null +++ b/lib/features/mailbox/presentation/widgets/empty_mailbox_dialog_overlay.dart @@ -0,0 +1,104 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/presentation/views/clipper/side_arrow_clipper.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/styles/empty_mailbox_dialog_overlay_styles.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_method_action_define.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EmptyMailboxDialogOverlay extends StatelessWidget { + + final MailboxNode mailboxNode; + final OnEmptyMailboxActionCallback onEmptyMailboxActionCallback; + final OnCancelActionClick onCancelActionClick; + + final _responsiveUtils = Get.find(); + final _imagePaths = Get.find(); + + EmptyMailboxDialogOverlay({ + Key? key, + required this.mailboxNode, + required this.onEmptyMailboxActionCallback, + required this.onCancelActionClick, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return PointerInterceptor( + child: PhysicalShape( + clipBehavior: Clip.antiAlias, + clipper: SideArrowClipper(), + color: EmptyMailboxDialogOverlayStyles.backgroundColor, + shadowColor: EmptyMailboxDialogOverlayStyles.shadowColor, + elevation: EmptyMailboxDialogOverlayStyles.elevation, + child: Container( + constraints: BoxConstraints( + maxHeight: _responsiveUtils.getSizeScreenHeight(context) + ), + width: EmptyMailboxDialogOverlayStyles.width, + padding: EmptyMailboxDialogOverlayStyles.padding, + margin: EmptyMailboxDialogOverlayStyles.margin, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: Text( + AppLocalizations.of(context).clearFolder, + style: EmptyMailboxDialogOverlayStyles.titleTextStyle + ), + ), + TMailButtonWidget.fromIcon( + icon: _imagePaths.icClose, + iconSize: EmptyMailboxDialogOverlayStyles.closeButtonIconSize, + iconColor: EmptyMailboxDialogOverlayStyles.closeButtonColor, + backgroundColor: Colors.transparent, + padding: EmptyMailboxDialogOverlayStyles.iconPadding, + onTapActionCallback: onCancelActionClick, + ) + ] + ), + const SizedBox(height: EmptyMailboxDialogOverlayStyles.space), + Flexible( + child: Text( + AppLocalizations.of(context).messageEmptyFolderDialog(mailboxNode.item.getDisplayName(context)), + style: EmptyMailboxDialogOverlayStyles.messageTextStyle + ), + ), + const SizedBox(height: EmptyMailboxDialogOverlayStyles.space), + Row( + children: [ + const Spacer(), + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).cancel, + backgroundColor: EmptyMailboxDialogOverlayStyles.cancelButtonColor, + padding: EmptyMailboxDialogOverlayStyles.buttonPadding, + textStyle: EmptyMailboxDialogOverlayStyles.buttonTextStyle, + borderRadius: EmptyMailboxDialogOverlayStyles.buttonRadius, + onTapActionCallback: onCancelActionClick, + ), + const SizedBox(width: EmptyMailboxDialogOverlayStyles.buttonSpace), + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).empty, + backgroundColor: EmptyMailboxDialogOverlayStyles.emptyButtonColor, + padding: EmptyMailboxDialogOverlayStyles.buttonPadding, + textStyle: EmptyMailboxDialogOverlayStyles.buttonTextStyle, + borderRadius: EmptyMailboxDialogOverlayStyles.buttonRadius, + onTapActionCallback: () => onEmptyMailboxActionCallback.call(mailboxNode), + ) + ], + ) + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/empty_mailbox_popup_dialog_widget.dart b/lib/features/mailbox/presentation/widgets/empty_mailbox_popup_dialog_widget.dart new file mode 100644 index 0000000000..dc68dee07a --- /dev/null +++ b/lib/features/mailbox/presentation/widgets/empty_mailbox_popup_dialog_widget.dart @@ -0,0 +1,62 @@ +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/styles/empty_mailbox_popup_dialog_widget_styles.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_method_action_define.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/empty_mailbox_dialog_overlay.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class EmptyMailboxPopupDialogWidget extends StatefulWidget { + + final MailboxNode mailboxNode; + final OnEmptyMailboxActionCallback onEmptyMailboxActionCallback; + + const EmptyMailboxPopupDialogWidget({ + super.key, + required this.mailboxNode, + required this.onEmptyMailboxActionCallback, + }); + + @override + State createState() => _EmptyMailboxPopupDialogWidgetState(); +} + +class _EmptyMailboxPopupDialogWidgetState extends State { + + bool _visible = false; + + @override + Widget build(BuildContext context) { + return PortalTarget( + visible: _visible, + portalFollower: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => setState(() => _visible = false) + ), + child: PortalTarget( + anchor: const Aligned( + follower: Alignment.bottomLeft, + target: Alignment.topRight, + offset: EmptyMailboxPopupDialogWidgetStyles.dialogOverlayOffset + ), + portalFollower: EmptyMailboxDialogOverlay( + mailboxNode: widget.mailboxNode, + onCancelActionClick: () => setState(() => _visible = false), + onEmptyMailboxActionCallback: (mailboxNode) { + setState(() => _visible = false); + widget.onEmptyMailboxActionCallback.call(mailboxNode); + }, + ), + visible: _visible, + child: TMailButtonWidget.fromText( + text: AppLocalizations.of(context).empty, + textStyle: EmptyMailboxPopupDialogWidgetStyles.emptyButtonTextStyle, + backgroundColor: EmptyMailboxPopupDialogWidgetStyles.emptyButtonBackground, + padding: EmptyMailboxPopupDialogWidgetStyles.emptyButtonPadding, + onTapActionCallback: () => setState(() => _visible = true) + ) + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/label_mailbox_item_widget.dart b/lib/features/mailbox/presentation/widgets/label_mailbox_item_widget.dart new file mode 100644 index 0000000000..0a1e464b9c --- /dev/null +++ b/lib/features/mailbox/presentation/widgets/label_mailbox_item_widget.dart @@ -0,0 +1,95 @@ +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/text/text_overflow_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/styles/label_mailbox_item_widget_styles.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_method_action_define.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/empty_mailbox_popup_dialog_widget.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/trailing_mailbox_item_widget.dart'; + +class LabelMailboxItemWidget extends StatelessWidget { + + final MailboxNode mailboxNode; + final bool showTrailing; + final bool isItemHovered; + final OnClickOpenMenuMailboxNodeAction? onMenuActionClick; + final OnEmptyMailboxActionCallback? onEmptyMailboxActionCallback; + + final _responsiveUtils = Get.find(); + + LabelMailboxItemWidget({ + super.key, + required this.mailboxNode, + this.showTrailing = true, + this.isItemHovered = false, + this.onMenuActionClick, + this.onEmptyMailboxActionCallback, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showTrailing) + Row( + children: [ + Expanded( + child: TextOverflowBuilder( + mailboxNode.item.getDisplayName(context), + style: TextStyle( + fontSize: _mailboxNameTextSize, + color: _mailboxNameTextColor, + fontWeight: _mailboxNameTextFontWeight + ), + ), + ), + if (_responsiveUtils.isWebDesktop(context) && mailboxNode.item.allowedHasEmptyAction) + EmptyMailboxPopupDialogWidget( + mailboxNode: mailboxNode, + onEmptyMailboxActionCallback: (mailboxNode) => onEmptyMailboxActionCallback?.call(mailboxNode), + ), + TrailingMailboxItemWidget( + mailboxNode: mailboxNode, + isItemHovered: isItemHovered, + onMenuActionClick: onMenuActionClick + ) + ], + ) + else + TextOverflowBuilder( + mailboxNode.item.getDisplayName(context), + style: TextStyle( + fontSize: _mailboxNameTextSize, + color: _mailboxNameTextColor, + fontWeight: _mailboxNameTextFontWeight + ), + ), + if (mailboxNode.item.isTeamMailboxes) + TextOverflowBuilder( + mailboxNode.item.emailTeamMailBoxes, + style: const TextStyle( + fontSize: LabelMailboxItemWidgetStyles.teamMailboxTextSize, + color: LabelMailboxItemWidgetStyles.teamMailboxTextColor, + fontWeight: LabelMailboxItemWidgetStyles.teamMailboxTextFontWeight + ), + ) + ], + ); + } + + double get _mailboxNameTextSize => mailboxNode.item.isTeamMailboxes + ? LabelMailboxItemWidgetStyles.labelTeamMailboxTextSize + : LabelMailboxItemWidgetStyles.labelFolderTextSize; + + Color get _mailboxNameTextColor => mailboxNode.item.isTeamMailboxes + ? LabelMailboxItemWidgetStyles.labelTeamMailboxTextColor + : LabelMailboxItemWidgetStyles.labelFolderTextColor; + + FontWeight get _mailboxNameTextFontWeight => mailboxNode.item.isTeamMailboxes + ? LabelMailboxItemWidgetStyles.labelTeamMailboxTextFontWeight + : LabelMailboxItemWidgetStyles.labelFolderTextFontWeight; +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/leading_mailbox_item_widget.dart b/lib/features/mailbox/presentation/widgets/leading_mailbox_item_widget.dart new file mode 100644 index 0000000000..98faeb38d9 --- /dev/null +++ b/lib/features/mailbox/presentation/widgets/leading_mailbox_item_widget.dart @@ -0,0 +1,88 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:model/mailbox/expand_mode.dart'; +import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/styles/leading_mailbox_item_widget_styles.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_method_action_define.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/mailbox_icon_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class LeadingMailboxItemWidget extends StatelessWidget { + + final MailboxNode mailboxNode; + final SelectMode selectionMode; + final OnSelectMailboxNodeAction? onSelectMailboxFolderClick; + final OnClickExpandMailboxNodeAction? onExpandFolderActionClick; + + const LeadingMailboxItemWidget({ + super.key, + required this.mailboxNode, + this.selectionMode = SelectMode.INACTIVE, + this.onExpandFolderActionClick, + this.onSelectMailboxFolderClick, + }); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + + return Row(children: [ + if (mailboxNode.hasChildren()) + Padding( + padding: LeadingMailboxItemWidgetStyles.expandIconPadding, + child: buildIconWeb( + icon: SvgPicture.asset( + _getExpandIcon(context, imagePaths), + colorFilter: _expandIconColor.asFilter(), + fit: BoxFit.fill, + ), + splashRadius: LeadingMailboxItemWidgetStyles.expandIconSplashRadius, + iconPadding: EdgeInsets.zero, + minSize: LeadingMailboxItemWidgetStyles.expandIconSize, + tooltip: _getExpandTooltipMessage(context), + onTap: () => onExpandFolderActionClick?.call(mailboxNode) + ), + ) + else + const SizedBox(width: LeadingMailboxItemWidgetStyles.emptyBoxSize), + Transform( + transform: LeadingMailboxItemWidgetStyles.mailboxIconTransform(context), + child: MailboxIconWidget( + mailboxNode: mailboxNode, + selectionMode: selectionMode, + onSelectMailboxFolderClick: onSelectMailboxFolderClick + ) + ), + ]); + } + + String _getExpandIcon(BuildContext context, ImagePaths imagePaths) { + if (mailboxNode.expandMode == ExpandMode.EXPAND) { + return imagePaths.icArrowBottom; + } else { + return DirectionUtils.isDirectionRTLByLanguage(context) + ? imagePaths.icArrowLeft + : imagePaths.icArrowRight; + } + } + + Color get _expandIconColor { + return mailboxNode.item.allowedToDisplay + ? LeadingMailboxItemWidgetStyles.displayColor + : LeadingMailboxItemWidgetStyles.normalColor; + } + + String _getExpandTooltipMessage(BuildContext context) { + return mailboxNode.expandMode == ExpandMode.EXPAND + ? AppLocalizations.of(context).collapse + : AppLocalizations.of(context).expand; + } +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/mailbox_bottom_sheet_action_tile_builder.dart b/lib/features/mailbox/presentation/widgets/mailbox_bottom_sheet_action_tile_builder.dart index c0100330dd..b87b940a74 100644 --- a/lib/features/mailbox/presentation/widgets/mailbox_bottom_sheet_action_tile_builder.dart +++ b/lib/features/mailbox/presentation/widgets/mailbox_bottom_sheet_action_tile_builder.dart @@ -41,7 +41,7 @@ class MailboxBottomSheetActionTileBuilder extends CupertinoActionSheetActionBuil child: Container( color: bgColor ?? Colors.white, child: MouseRegion( - cursor: BuildUtils.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( key: key, child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/features/mailbox/presentation/widgets/mailbox_button_new_folder_builder.dart b/lib/features/mailbox/presentation/widgets/mailbox_button_new_folder_builder.dart deleted file mode 100644 index 5cdbd4a411..0000000000 --- a/lib/features/mailbox/presentation/widgets/mailbox_button_new_folder_builder.dart +++ /dev/null @@ -1,66 +0,0 @@ - -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; - -typedef OnOpenMailboxNewFolderActionClick = void Function(); - -class MailboxNewFolderTileBuilder { - - String? _icon; - String? _name; - - OnOpenMailboxNewFolderActionClick? _onOpenMailboxFolderActionClick; - - MailboxNewFolderTileBuilder(); - - void addIcon(String icon) { - _icon = icon; - } - - void addName(String name) { - _name = name; - } - - void onOpenMailboxFolderAction(OnOpenMailboxNewFolderActionClick onOpenMailboxFolderActionClick) { - _onOpenMailboxFolderActionClick = onOpenMailboxFolderActionClick; - } - - Widget build() { - return Theme( - data: ThemeData( - splashColor: Colors.transparent, - highlightColor: Colors.transparent), - child: Container( - key: const Key('mailbox_new_folder_tile'), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(16), - color: AppColor.mailboxBackgroundColor), - child: MediaQuery( - data: const MediaQueryData(padding: EdgeInsets.zero), - child: ListTile( - contentPadding: EdgeInsets.zero, - onTap: () => { - if (_onOpenMailboxFolderActionClick != null) { - _onOpenMailboxFolderActionClick!() - } - }, - leading: Padding( - padding: const EdgeInsets.only(left: 34), - child: _icon != null - ? SvgPicture.asset(_icon!, width: 24, height: 24, color: AppColor.mailboxIconColor, fit: BoxFit.fill) - : const SizedBox.shrink()), - title: Padding( - padding: const EdgeInsets.only(left: 8), - child: Text( - _name ?? '', - maxLines: 1, - style: const TextStyle(fontSize: 15, color: AppColor.mailboxTextColor, fontWeight: FontWeight.bold), - )), - ) - ) - ) - ); - } -} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart b/lib/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart deleted file mode 100644 index 4c4a22aec9..0000000000 --- a/lib/features/mailbox/presentation/widgets/mailbox_folder_tile_builder.dart +++ /dev/null @@ -1,413 +0,0 @@ -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/extensions/presentation_mailbox_extension.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_displayed.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_method_action_define.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -class MailBoxFolderTileBuilder { - - final _responsiveUtils = Get.find(); - - final MailboxNode _mailboxNode; - final BuildContext _context; - final ImagePaths _imagePaths; - final SelectMode allSelectMode; - final MailboxDisplayed mailboxDisplayed; - final MailboxNode? lastNode; - final PresentationMailbox? mailboxNodeSelected; - final MailboxActions? mailboxActions; - final MailboxId? mailboxIdAlreadySelected; - - OnClickExpandMailboxNodeAction? _onExpandFolderActionClick; - OnClickOpenMailboxNodeAction? _onOpenMailboxFolderClick; - OnSelectMailboxNodeAction? _onSelectMailboxFolderClick; - OnClickOpenMenuMailboxNodeAction? _onMenuActionClick; - OnDragEmailToMailboxAccepted? _onDragItemAccepted; - OnLongPressMailboxNodeAction? _onLongPressMailboxNodeAction; - - bool isHoverItem = false; - - MailBoxFolderTileBuilder( - this._context, - this._imagePaths, - this._mailboxNode, - { - this.allSelectMode = SelectMode.INACTIVE, - this.mailboxDisplayed = MailboxDisplayed.mailbox, - this.lastNode, - this.mailboxNodeSelected, - this.mailboxActions, - this.mailboxIdAlreadySelected - } - ); - - void addOnClickExpandMailboxNodeAction(OnClickExpandMailboxNodeAction onExpandFolderActionClick) { - _onExpandFolderActionClick = onExpandFolderActionClick; - } - - void addOnClickOpenMailboxNodeAction(OnClickOpenMailboxNodeAction onOpenMailboxFolderClick) { - _onOpenMailboxFolderClick = onOpenMailboxFolderClick; - } - - void addOnSelectMailboxNodeAction(OnSelectMailboxNodeAction onSelectMailboxFolderClick) { - _onSelectMailboxFolderClick = onSelectMailboxFolderClick; - } - - void addOnClickOpenMenuMailboxNodeAction(OnClickOpenMenuMailboxNodeAction onMenuActionClick) { - _onMenuActionClick = onMenuActionClick; - } - - void addOnDragEmailToMailboxAccepted(OnDragEmailToMailboxAccepted onDragItemAccepted) { - _onDragItemAccepted = onDragItemAccepted; - } - - void addOnLongPressMailboxNodeAction(OnLongPressMailboxNodeAction onLongPressMailboxNodeAction) { - _onLongPressMailboxNodeAction = onLongPressMailboxNodeAction; - } - - Widget build() { - if (BuildUtils.isWeb) { - return DragTarget>( - builder: (context, _, __,) { - return _buildMailboxItem(); - }, - onAccept: (emails) { - _onDragItemAccepted?.call(emails, _mailboxNode.item); - }, - ); - } else { - return _buildMailboxItem(); - } - } - - Widget _buildMailboxItem() { - if (mailboxDisplayed == MailboxDisplayed.mailbox) { - if (BuildUtils.isWeb) { - return Theme( - data: ThemeData( - splashColor: Colors.transparent, - highlightColor: Colors.transparent), - child: StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return InkWell( - onTap: () => _onOpenMailboxFolderClick?.call(_mailboxNode), - onHover: (value) => setState(() => isHoverItem = value), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: backgroundColorItem), - padding: EdgeInsets.only( - left: _mailboxNode.item.hasRole() ? 0 : 4, - right: 4, - top: 8, - bottom: 8), - margin: const EdgeInsets.only(bottom: 4), - child: Row( - crossAxisAlignment: _mailboxNode.item.isTeamMailboxes - ? CrossAxisAlignment.start - : CrossAxisAlignment.center, - children: [ - _buildLeadingMailboxItem(), - const SizedBox(width: 4), - Expanded(child: _buildTitleFolderItem()), - const SizedBox(width: 8), - _buildTrailingItemForMailboxView() - ]) - ), - ); - } - ), - ); - } else { - return AbsorbPointer( - absorbing: !_mailboxNode.isActivated, - child: Opacity( - opacity: _mailboxNode.isActivated ? 1.0 : 0.3, - child: Material( - color: Colors.transparent, - child: InkWell( - onLongPress: () => _onLongPressMailboxNodeAction?.call(_mailboxNode), - onTap: () => allSelectMode == SelectMode.ACTIVE - ? _onSelectMailboxFolderClick?.call(_mailboxNode) - : _onOpenMailboxFolderClick?.call(_mailboxNode), - borderRadius: const BorderRadius.all(Radius.circular(8)), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(14)), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: EdgeInsets.symmetric( - vertical: _mailboxNode.hasChildren() ? 8 : 15), - child: Row( - crossAxisAlignment: _mailboxNode.item.isTeamMailboxes - ? CrossAxisAlignment.start - : CrossAxisAlignment.center, - children: [ - _buildLeadingMailboxItem(), - const SizedBox(width: 8), - Expanded(child: _buildTitleFolderItem()), - _buildSelectedIcon(), - const SizedBox(width: 8), - _buildTrailingItemForMailboxView() - ]), - ), - ] - ) - ), - ), - ), - ), - ); - } - } else { - return AbsorbPointer( - absorbing: !_mailboxNode.isActivated, - child: Opacity( - opacity: _mailboxNode.isActivated ? 1.0 : 0.3, - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () => _onOpenMailboxFolderClick?.call(_mailboxNode), - customBorder: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8))), - hoverColor: AppColor.colorMailboxHovered, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 8), - color: _mailboxNode.isSelected - ? AppColor.colorItemSelected - : Colors.transparent, - child: Row(children: [ - _buildLeadingMailboxItem(), - const SizedBox(width: 8), - Expanded(child: _buildTitleFolderItem()), - _buildSelectedIcon(), - const SizedBox(width: 8) - ]) - ), - ), - ), - ), - ); - } - } - - Widget _buildLeadingMailboxItem() { - return Row(children: [ - if (_mailboxNode.hasChildren()) - Row(children: [ - const SizedBox(width: 8), - buildIconWeb( - icon: SvgPicture.asset( - _mailboxNode.expandMode == ExpandMode.EXPAND - ? _imagePaths.icExpandFolder - : _imagePaths.icCollapseFolder, - color: _mailboxNode.expandMode == ExpandMode.EXPAND - ? AppColor.colorExpandMailbox - : AppColor.colorCollapseMailbox, - fit: BoxFit.fill - ), - minSize: 12, - splashRadius: 10, - iconPadding: EdgeInsets.zero, - tooltip: _mailboxNode.expandMode == ExpandMode.EXPAND - ? AppLocalizations.of(_context).collapse - : AppLocalizations.of(_context).expand, - onTap: () => _onExpandFolderActionClick?.call(_mailboxNode) - ), - ]) - else - const SizedBox(width: 32), - Transform( - transform: Matrix4.translationValues(-4.0, 0.0, 0.0), - child: _buildLeadingIcon() - ), - ]); - } - - Widget _buildTrailingItemForMailboxView() { - if (BuildUtils.isWeb) { - if (isHoverItem) { - return _buildMenuIcon(); - } else if (_mailboxNode.item.getCountUnReadEmails().isNotEmpty - && _mailboxNode.item.matchCountingRules()) { - return Padding( - padding: const EdgeInsets.only(right: 10), - child: _buildCounter(), - ); - } else { - return const SizedBox(width: 20); - } - } else { - if (_mailboxNode.hasChildren()) { - return Padding( - padding: const EdgeInsets.only(right: 12), - child: Row( - children: [ - if (_mailboxNode.item.getCountUnReadEmails().isNotEmpty - && _mailboxNode.item.matchCountingRules()) - _buildCounter(), - ], - ), - ); - } else if (_mailboxNode.item.getCountUnReadEmails().isNotEmpty - && _mailboxNode.item.matchCountingRules()) { - return Padding( - padding: const EdgeInsets.only(right: 12), - child: _buildCounter(), - ); - } else { - return const SizedBox(); - } - } - } - - Widget _buildLeadingIcon() { - if (BuildUtils.isWeb) { - return _buildLeadingIconTeamMailboxes(); - } else { - return allSelectMode == SelectMode.ACTIVE - ? _buildSelectModeIcon() - : _buildLeadingIconTeamMailboxes(); - } - } - - Widget _buildLeadingIconTeamMailboxes() { - if (!_mailboxNode.item.isPersonal) { - return _buildLeadingIconForChildOfTeamMailboxes(); - } else { - return _buildMailboxIcon(); - } - } - - Widget _buildLeadingIconForChildOfTeamMailboxes() { - if (_mailboxNode.item.hasParentId()) { - return _buildMailboxIcon(); - } else { - return const SizedBox(); - } - } - - Widget _buildTitleFolderItem() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _mailboxNode.item.name?.name ?? '', - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: TextStyle( - fontSize: _mailboxNode.item.isTeamMailboxes ? 16 : 15, - color: _mailboxNode.item.isTeamMailboxes ? Colors.black : AppColor.colorNameEmail, - fontWeight: _mailboxNode.item.isTeamMailboxes ? FontWeight.bold : FontWeight.normal), - ), - if(_mailboxNode.item.isTeamMailboxes) - Text( - _mailboxNode.item.emailTeamMailBoxes ?? '', - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: const TextStyle( - fontSize: 13, - color: AppColor.colorEmailAddressFull, - fontWeight: FontWeight.w400), - ) - ], - ); - } - - Widget _buildCounter() { - return Text( - _mailboxNode.item.getCountUnReadEmails(), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - style: const TextStyle( - fontSize: 13, - color: Colors.black, - fontWeight: FontWeight.normal), - ); - } - - Widget _buildMailboxIcon() { - return SvgPicture.asset(_mailboxNode.item.getMailboxIcon(_imagePaths), - width: BuildUtils.isWeb ? 20 : 24, - height: BuildUtils.isWeb ? 20 : 24, - fit: BoxFit.fill); - } - - Widget _buildMenuIcon() { - return Padding( - padding: const EdgeInsets.only(right: 8), - child: InkWell( - onTapDown: (detail) { - final screenSize = MediaQuery.of(_context).size; - final offset = detail.globalPosition; - final position = RelativeRect.fromLTRB( - offset.dx, - offset.dy, - screenSize.width - offset.dx, - screenSize.height - offset.dy, - ); - _onMenuActionClick?.call(position, _mailboxNode); - }, - onTap: () => {}, - child: SvgPicture.asset(_imagePaths.icComposerMenu, - width: 20, - height: 20, - fit: BoxFit.fill)), - ); - } - - Widget _buildSelectModeIcon() { - return InkWell( - onTap: () => _onSelectMailboxFolderClick?.call(_mailboxNode), - child: SvgPicture.asset( - _mailboxNode.selectMode == SelectMode.ACTIVE - ? _imagePaths.icSelected - : _imagePaths.icUnSelected, - width: BuildUtils.isWeb ? 20 : 24, - height: BuildUtils.isWeb ? 20 : 24, - fit: BoxFit.fill) - ); - } - - Color get backgroundColorItem { - if (mailboxDisplayed == MailboxDisplayed.destinationPicker) { - return Colors.white; - } else { - if (BuildUtils.isWeb) { - if (mailboxNodeSelected?.id == _mailboxNode.item.id || isHoverItem) { - return AppColor.colorBgMailboxSelected; - } - return _responsiveUtils.isDesktop(_context) - ? AppColor.colorBgDesktop - : Colors.white; - } else { - return Colors.white; - } - } - } - - Widget _buildSelectedIcon() { - if (_mailboxNode.item.id == mailboxIdAlreadySelected && - mailboxDisplayed == MailboxDisplayed.destinationPicker && - (mailboxActions == MailboxActions.select || - mailboxActions == MailboxActions.create)) { - return SvgPicture.asset( - _imagePaths.icFilterSelected, - width: 20, - height: 20, - fit: BoxFit.fill); - } else { - return const SizedBox.shrink(); - } - } -} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/mailbox_icon_widget.dart b/lib/features/mailbox/presentation/widgets/mailbox_icon_widget.dart new file mode 100644 index 0000000000..bd3abe274b --- /dev/null +++ b/lib/features/mailbox/presentation/widgets/mailbox_icon_widget.dart @@ -0,0 +1,62 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/styles/mailbox_icon_widget_styles.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_method_action_define.dart'; + +class MailboxIconWidget extends StatelessWidget { + + final MailboxNode mailboxNode; + final SelectMode selectionMode; + final OnSelectMailboxNodeAction? onSelectMailboxFolderClick; + + const MailboxIconWidget({ + super.key, + required this.mailboxNode, + this.selectionMode = SelectMode.INACTIVE, + this.onSelectMailboxFolderClick, + }); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + + if (_isSelectionActivatedOnMobile) { + return InkWell( + onTap: () => onSelectMailboxFolderClick?.call(mailboxNode), + child: SvgPicture.asset( + _getSelectionIcon(imagePaths), + width: MailboxIconWidgetStyles.iconSize, + height: MailboxIconWidgetStyles.iconSize, + fit: BoxFit.fill + ) + ); + } else { + if (mailboxNode.item.isPersonal || mailboxNode.item.hasParentId()) { + return SvgPicture.asset( + mailboxNode.item.getMailboxIcon(imagePaths), + width: MailboxIconWidgetStyles.iconSize, + height: MailboxIconWidgetStyles.iconSize, + fit: BoxFit.fill + ); + } else { + return const SizedBox(); + } + } + } + + bool get _isSelectionActivatedOnMobile => PlatformInfo.isMobile && selectionMode == SelectMode.ACTIVE; + + String _getSelectionIcon(ImagePaths imagePaths) { + return mailboxNode.selectMode == SelectMode.ACTIVE + ? imagePaths.icSelected + : imagePaths.icUnSelected; + } +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/mailbox_item_widget.dart b/lib/features/mailbox/presentation/widgets/mailbox_item_widget.dart new file mode 100644 index 0000000000..51f95f6e03 --- /dev/null +++ b/lib/features/mailbox/presentation/widgets/mailbox_item_widget.dart @@ -0,0 +1,261 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_displayed.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/styles/mailbox_item_widget_styles.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_method_action_define.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/label_mailbox_item_widget.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/leading_mailbox_item_widget.dart'; + +class MailboxItemWidget extends StatefulWidget { + + final MailboxNode mailboxNode; + final SelectMode selectionMode; + final MailboxDisplayed mailboxDisplayed; + final PresentationMailbox? mailboxNodeSelected; + final MailboxActions? mailboxActions; + final MailboxId? mailboxIdAlreadySelected; + + final OnClickExpandMailboxNodeAction? onExpandFolderActionClick; + final OnClickOpenMailboxNodeAction? onOpenMailboxFolderClick; + final OnSelectMailboxNodeAction? onSelectMailboxFolderClick; + final OnClickOpenMenuMailboxNodeAction? onMenuActionClick; + final OnDragEmailToMailboxAccepted? onDragItemAccepted; + final OnLongPressMailboxNodeAction? onLongPressMailboxNodeAction; + final OnEmptyMailboxActionCallback? onEmptyMailboxActionCallback; + + const MailboxItemWidget({ + super.key, + required this.mailboxNode, + this.selectionMode = SelectMode.INACTIVE, + this.mailboxDisplayed = MailboxDisplayed.mailbox, + this.mailboxNodeSelected, + this.mailboxActions, + this.mailboxIdAlreadySelected, + this.onExpandFolderActionClick, + this.onOpenMailboxFolderClick, + this.onSelectMailboxFolderClick, + this.onMenuActionClick, + this.onDragItemAccepted, + this.onLongPressMailboxNodeAction, + this.onEmptyMailboxActionCallback, + }); + + @override + State createState() => _MailboxItemWidgetState(); +} + +class _MailboxItemWidgetState extends State { + + final _responsiveUtils = Get.find(); + final _imagePaths = Get.find(); + + bool _isItemHovered = false; + + @override + Widget build(BuildContext context) { + if (_responsiveUtils.isWebDesktop(context) && widget.mailboxDisplayed == MailboxDisplayed.mailbox) { + return DragTarget>( + builder: (context, candidateEmails, rejectedEmails) { + return InkWell( + onTap: () => widget.onOpenMailboxFolderClick?.call(widget.mailboxNode), + onHover: (value) => setState(() => _isItemHovered = value), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(MailboxItemWidgetStyles.borderRadius)), + color: backgroundColorItem + ), + padding: const EdgeInsets.all(MailboxItemWidgetStyles.padding), + child: Row( + children: [ + LeadingMailboxItemWidget( + mailboxNode: widget.mailboxNode, + selectionMode: widget.selectionMode, + onSelectMailboxFolderClick: widget.onSelectMailboxFolderClick, + onExpandFolderActionClick: widget.onExpandFolderActionClick, + ), + const SizedBox(width: MailboxItemWidgetStyles.space), + Expanded( + child: LabelMailboxItemWidget( + mailboxNode: widget.mailboxNode, + isItemHovered: _isItemHovered, + onMenuActionClick: widget.onMenuActionClick, + onEmptyMailboxActionCallback: widget.onEmptyMailboxActionCallback + ) + ), + ] + ) + ), + ); + }, + onAccept: (emails) => widget.onDragItemAccepted?.call(emails, widget.mailboxNode.item), + ); + } else { + if (widget.mailboxDisplayed == MailboxDisplayed.mailbox) { + if (PlatformInfo.isWeb) { + return InkWell( + onTap: () => widget.onOpenMailboxFolderClick?.call(widget.mailboxNode), + onHover: (value) => setState(() => _isItemHovered = value), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(MailboxItemWidgetStyles.borderRadius)), + color: backgroundColorItem + ), + padding: const EdgeInsets.all(MailboxItemWidgetStyles.padding), + child: Row( + children: [ + LeadingMailboxItemWidget( + mailboxNode: widget.mailboxNode, + selectionMode: widget.selectionMode, + onSelectMailboxFolderClick: widget.onSelectMailboxFolderClick, + onExpandFolderActionClick: widget.onExpandFolderActionClick, + ), + const SizedBox(width: MailboxItemWidgetStyles.space), + Expanded( + child: LabelMailboxItemWidget( + mailboxNode: widget.mailboxNode, + isItemHovered: _isItemHovered, + onMenuActionClick: widget.onMenuActionClick, + onEmptyMailboxActionCallback: widget.onEmptyMailboxActionCallback + ) + ), + ] + ) + ), + ); + } else { + return Material( + color: Colors.transparent, + child: InkWell( + onLongPress: () => widget.onLongPressMailboxNodeAction?.call(widget.mailboxNode), + onTap: () => widget.selectionMode == SelectMode.ACTIVE + ? widget.onSelectMailboxFolderClick?.call(widget.mailboxNode) + : widget.onOpenMailboxFolderClick?.call(widget.mailboxNode), + borderRadius: const BorderRadius.all(Radius.circular(MailboxItemWidgetStyles.borderRadius)), + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(MailboxItemWidgetStyles.borderRadius)), + color: backgroundColorItem + ), + padding: const EdgeInsets.all(MailboxItemWidgetStyles.padding), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + children: [ + LeadingMailboxItemWidget( + mailboxNode: widget.mailboxNode, + selectionMode: widget.selectionMode, + onSelectMailboxFolderClick: widget.onSelectMailboxFolderClick, + onExpandFolderActionClick: widget.onExpandFolderActionClick, + ), + const SizedBox(width: MailboxItemWidgetStyles.padding), + Expanded( + child: LabelMailboxItemWidget( + mailboxNode: widget.mailboxNode, + isItemHovered: _isItemHovered, + onMenuActionClick: widget.onMenuActionClick, + onEmptyMailboxActionCallback: widget.onEmptyMailboxActionCallback + ) + ), + if (_isSelectActionNoValid) + SvgPicture.asset( + _imagePaths.icSelectedSB, + width: MailboxItemWidgetStyles.selectionIconSize, + height: MailboxItemWidgetStyles.selectionIconSize, + fit: BoxFit.fill + ) + ] + ), + ] + ), + ), + ), + ); + } + } else { + return AbsorbPointer( + absorbing: !widget.mailboxNode.isActivated, + child: Opacity( + opacity: widget.mailboxNode.isActivated ? 1.0 : 0.3, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => !_isSelectActionNoValid + ? widget.onOpenMailboxFolderClick?.call(widget.mailboxNode) + : null, + customBorder: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(8))), + hoverColor: AppColor.colorMailboxHovered, + child: Container( + padding: const EdgeInsets.symmetric(vertical: MailboxItemWidgetStyles.padding), + color: widget.mailboxNode.isSelected + ? AppColor.colorItemSelected + : Colors.transparent, + child: Row( + children: [ + LeadingMailboxItemWidget( + mailboxNode: widget.mailboxNode, + selectionMode: widget.selectionMode, + onSelectMailboxFolderClick: widget.onSelectMailboxFolderClick, + onExpandFolderActionClick: widget.onExpandFolderActionClick, + ), + const SizedBox(width: MailboxItemWidgetStyles.padding), + Expanded( + child: LabelMailboxItemWidget( + mailboxNode: widget.mailboxNode, + showTrailing: false, + isItemHovered: _isItemHovered, + onMenuActionClick: widget.onMenuActionClick, + onEmptyMailboxActionCallback: widget.onEmptyMailboxActionCallback + ) + ), + if (_isSelectActionNoValid) + SvgPicture.asset( + _imagePaths.icSelectedSB, + width: MailboxItemWidgetStyles.selectionIconSize, + height: MailboxItemWidgetStyles.selectionIconSize, + fit: BoxFit.fill + ), + const SizedBox(width: MailboxItemWidgetStyles.padding), + ] + ) + ), + ), + ), + ), + ); + } + } + } + + Color get backgroundColorItem { + if (widget.mailboxDisplayed == MailboxDisplayed.destinationPicker) { + return Colors.white; + } else { + if (widget.mailboxNodeSelected?.id == widget.mailboxNode.item.id || _isItemHovered) { + return AppColor.colorBgMailboxSelected; + } else { + return Colors.transparent; + } + } + } + + bool get _isSelectActionNoValid => widget.mailboxNode.item.id == widget.mailboxIdAlreadySelected && + widget.mailboxDisplayed == MailboxDisplayed.destinationPicker && + ( + widget.mailboxActions == MailboxActions.select || + widget.mailboxActions == MailboxActions.create || + widget.mailboxActions == MailboxActions.moveEmail + ); +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/mailbox_loading_bar_widget.dart b/lib/features/mailbox/presentation/widgets/mailbox_loading_bar_widget.dart new file mode 100644 index 0000000000..32f19a4dde --- /dev/null +++ b/lib/features/mailbox/presentation/widgets/mailbox_loading_bar_widget.dart @@ -0,0 +1,33 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/widget/cupertino_loading_widget.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/styles/mailbox_loading_bar_widget_styles.dart'; + +class MailboxLoadingBarWidget extends StatelessWidget { + + final Either viewState; + + const MailboxLoadingBarWidget({ + super.key, + required this.viewState, + }); + + @override + Widget build(BuildContext context) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is GetAllMailboxLoading) { + return const Padding( + padding: MailboxLoadingBarWidgetStyles.padding, + child: CupertinoLoadingWidget()); + } else { + return const SizedBox.shrink(); + } + } + ); + } +} diff --git a/lib/features/mailbox/presentation/widgets/sending_queue_mailbox_widget.dart b/lib/features/mailbox/presentation/widgets/sending_queue_mailbox_widget.dart new file mode 100644 index 0000000000..8ac213f4bd --- /dev/null +++ b/lib/features/mailbox/presentation/widgets/sending_queue_mailbox_widget.dart @@ -0,0 +1,98 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/text/text_overflow_builder.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class SendingQueueMailboxWidget extends StatelessWidget { + + final List listSendingEmails; + final bool isSelected; + final VoidCallback? onOpenSendingQueueAction; + + const SendingQueueMailboxWidget({ + super.key, + required this.listSendingEmails, + this.isSelected = false, + this.onOpenSendingQueueAction + }); + + @override + Widget build(BuildContext context) { + final imagePath = Get.find(); + final responsiveUtils = Get.find(); + + return Padding( + padding: const EdgeInsets.only(left: 20, right: 16), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onOpenSendingQueueAction, + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: isSelected ? AppColor.colorBgMailboxSelected : Colors.transparent), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(width: responsiveUtils.isLandscapeMobile(context) ? 20 : 28), + SvgPicture.asset( + imagePath.icMailboxSendingQueue, + width: PlatformInfo.isWeb ? 20 : 24, + height: PlatformInfo.isWeb ? 20 : 24, + colorFilter: AppColor.primaryColor.asFilter(), + fit: BoxFit.fill), + const SizedBox(width: 12), + Expanded(child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: TextOverflowBuilder( + AppLocalizations.of(context).sendingQueue, + style: const TextStyle( + fontSize: 15, + color: AppColor.colorNameEmail, + fontWeight: FontWeight.normal), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 12), + child: TextOverflowBuilder( + _getCountSendingEmails(), + style: const TextStyle( + fontSize: 13, + color: Colors.black, + fontWeight: FontWeight.normal + ) + ), + ) + ], + )) + ] + ), + ] + ), + ), + ), + ), + ); + } + + String _getCountSendingEmails() { + if (listSendingEmails.isEmpty) { + return ''; + } + return listSendingEmails.length <= 999 ? '${listSendingEmails.length}' : '999+'; + } +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/trailing_mailbox_item_widget.dart b/lib/features/mailbox/presentation/widgets/trailing_mailbox_item_widget.dart new file mode 100644 index 0000000000..3c3498d67b --- /dev/null +++ b/lib/features/mailbox/presentation/widgets/trailing_mailbox_item_widget.dart @@ -0,0 +1,75 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/styles/trailing_mailbox_item_widget_styles.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_method_action_define.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/count_of_emails_widget.dart'; + +class TrailingMailboxItemWidget extends StatelessWidget { + + final MailboxNode mailboxNode; + final bool isItemHovered; + final OnClickOpenMenuMailboxNodeAction? onMenuActionClick; + + final _imagePaths = Get.find(); + final _responsiveUtils = Get.find(); + + TrailingMailboxItemWidget({ + super.key, + required this.mailboxNode, + this.isItemHovered = false, + this.onMenuActionClick, + }); + + @override + Widget build(BuildContext context) { + if (PlatformInfo.isWeb) { + if (isItemHovered) { + return TMailButtonWidget.fromIcon( + margin: _responsiveUtils.isDesktop(context) && mailboxNode.item.allowedHasEmptyAction + ? EdgeInsets.zero + : TrailingMailboxItemWidgetStyles.menuIconMargin, + icon: _imagePaths.icComposerMenu, + iconSize: TrailingMailboxItemWidgetStyles.menuIconSize, + padding: TrailingMailboxItemWidgetStyles.menuIconPadding, + backgroundColor: TrailingMailboxItemWidgetStyles.menuIconBackgroundColor, + onTapActionAtPositionCallback: (position) => onMenuActionClick?.call(position, mailboxNode), + ); + } else if (_responsiveUtils.isDesktop(context) && mailboxNode.item.allowedHasEmptyAction) { + return const SizedBox(); + } else if (mailboxNode.item.allowedToDisplayCountOfUnreadEmails) { + return Padding( + padding: TrailingMailboxItemWidgetStyles.countEmailsPadding, + child: CountOfEmailsWidget(value: mailboxNode.item.countUnReadEmailsAsString), + ); + } else if (mailboxNode.item.allowedToDisplayCountOfTotalEmails) { + return Padding( + padding: TrailingMailboxItemWidgetStyles.countEmailsPadding, + child: CountOfEmailsWidget(value: mailboxNode.item.countTotalEmailsAsString), + ); + } else { + return const SizedBox(); + } + } else { + if (mailboxNode.item.allowedToDisplayCountOfUnreadEmails) { + return Padding( + padding: TrailingMailboxItemWidgetStyles.countEmailsPadding, + child: CountOfEmailsWidget(value: mailboxNode.item.countUnReadEmailsAsString), + ); + } else if (mailboxNode.item.allowedToDisplayCountOfTotalEmails) { + return Padding( + padding: TrailingMailboxItemWidgetStyles.countEmailsPadding, + child: CountOfEmailsWidget(value: mailboxNode.item.countTotalEmailsAsString), + ); + } else { + return const SizedBox(); + } + } + } +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/user_information_widget.dart b/lib/features/mailbox/presentation/widgets/user_information_widget.dart new file mode 100644 index 0000000000..d4b1d10968 --- /dev/null +++ b/lib/features/mailbox/presentation/widgets/user_information_widget.dart @@ -0,0 +1,88 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/image/avatar_builder.dart'; +import 'package:core/presentation/views/text/text_overflow_builder.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:model/user/user_profile.dart'; +import 'package:tmail_ui_user/features/base/widget/material_text_button.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnSubtitleClick = void Function(); + +class UserInformationWidget extends StatelessWidget { + final UserProfile? userProfile; + final String? subtitle; + final EdgeInsetsGeometry? titlePadding; + final OnSubtitleClick? onSubtitleClick; + final EdgeInsetsGeometry? padding; + final Border? border; + + const UserInformationWidget({ + Key? key, + this.userProfile, + this.subtitle, + this.titlePadding, + this.onSubtitleClick, + this.padding, + this.border, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + return Container( + padding: padding ?? const EdgeInsetsDirectional.only(start: 16, end: 4, top: 16, bottom: 16), + decoration: BoxDecoration(border: border), + child: Row(children: [ + (AvatarBuilder() + ..text(userProfile != null ? userProfile!.getAvatarText() : '') + ..backgroundColor(Colors.white) + ..textColor(Colors.black) + ..addBoxShadows([const BoxShadow( + color: AppColor.colorShadowBgContentEmail, + spreadRadius: 1, blurRadius: 1, offset: Offset(0, 0.5))]) + ..size(PlatformInfo.isWeb ? 48 : 56)) + .build(), + const SizedBox(width: 16), + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextOverflowBuilder( + userProfile != null ? '${userProfile?.email}' : '', + style: const TextStyle( + fontSize: 17, + color: AppColor.colorNameEmail, + fontWeight: FontWeight.w600 + ) + ), + if (subtitle != null) + Padding( + padding: const EdgeInsetsDirectional.only(top: 10), + child: Transform( + transform: Matrix4.translationValues(-8.0, 0.0, 0.0), + child: MaterialTextButton( + label: AppLocalizations.of(context).manage_account, + onTap: onSubtitleClick, + borderRadius: 20, + padding: const EdgeInsetsDirectional.symmetric(horizontal: 8, vertical: 8), + customStyle: const TextStyle(fontSize: 14, color: AppColor.colorTextButton), + ), + ), + ) + ])), + if (PlatformInfo.isMobile) + SvgPicture.asset( + DirectionUtils.isDirectionRTLByLanguage(context) ? imagePaths.icBack : imagePaths.icCollapseFolder, + fit: BoxFit.fill, + colorFilter: AppColor.colorCollapseMailbox.asFilter() + ), + const SizedBox(width: 16), + ]), + ); + } +} \ No newline at end of file diff --git a/lib/features/mailbox/presentation/widgets/user_information_widget_builder.dart b/lib/features/mailbox/presentation/widgets/user_information_widget_builder.dart deleted file mode 100644 index 893884f49b..0000000000 --- a/lib/features/mailbox/presentation/widgets/user_information_widget_builder.dart +++ /dev/null @@ -1,89 +0,0 @@ - -import 'package:core/core.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get_utils/src/platform/platform.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -typedef OnSubtitleClick = void Function(); - -class UserInformationWidgetBuilder extends StatelessWidget { - final ImagePaths _imagePaths; - final UserProfile? _userProfile; - final String? subtitle; - final EdgeInsets? titlePadding; - final OnSubtitleClick? onSubtitleClick; - - const UserInformationWidgetBuilder( - this._imagePaths, - this._userProfile, - { - Key? key, - this.subtitle, - this.titlePadding, - this.onSubtitleClick, - } - ) : super(key: key); - - @override - Widget build(BuildContext context) { - return Container( - key: const Key('user_information_widget'), - color: Colors.transparent, - padding: EdgeInsets.zero, - margin: EdgeInsets.zero, - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - (AvatarBuilder() - ..text(_userProfile != null ? _userProfile!.getAvatarText() : '') - ..backgroundColor(Colors.white) - ..textColor(Colors.black) - ..addBoxShadows([const BoxShadow( - color: AppColor.colorShadowBgContentEmail, - spreadRadius: 1, blurRadius: 1, offset: Offset(0, 0.5))]) - ..size(GetPlatform.isWeb ? 48 : 56)) - .build(), - Expanded(child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: titlePadding ?? const EdgeInsets.only(left: 16, top: 10), - child: Text( - _userProfile != null ? '${_userProfile?.email}' : '', - maxLines: 1, - overflow: GetPlatform.isWeb ? TextOverflow.clip : TextOverflow.ellipsis, - style: const TextStyle(fontSize: 17, color: AppColor.colorNameEmail, fontWeight: FontWeight.w600) - ) - ), - subtitle != null - ? Padding( - padding: const EdgeInsets.only(left: 10), - child: Material( - borderRadius: BorderRadius.circular(20), - color: Colors.transparent, - child: Align( - alignment: Alignment.centerLeft, - child: TextButton( - onPressed: () => onSubtitleClick?.call(), - child: Text( - AppLocalizations.of(context).manage_account, - style: const TextStyle(fontSize: 14, color: AppColor.colorTextButton), - ), - ) - ) - ) - ) - : const SizedBox.shrink() - ])), - if (!kIsWeb) - Transform( - transform: Matrix4.translationValues(14.0, 0.0, 0.0), - child: IconButton( - icon: SvgPicture.asset(_imagePaths.icCollapseFolder, fit: BoxFit.fill, color: AppColor.colorCollapseMailbox), - onPressed: () => {})) - ]), - ); - } -} \ No newline at end of file diff --git a/lib/features/mailbox_creator/domain/model/verification/composite_name_validator.dart b/lib/features/mailbox_creator/domain/model/verification/composite_name_validator.dart index eb7e678b4a..34690b2c19 100644 --- a/lib/features/mailbox_creator/domain/model/verification/composite_name_validator.dart +++ b/lib/features/mailbox_creator/domain/model/verification/composite_name_validator.dart @@ -13,9 +13,9 @@ class CompositeNameValidator extends Validator { CompositeNameValidator(this._listValidator); @override - Either validate(NewNameRequest newNameRequest) { + Either validate(NewNameRequest value) { return _listValidator.isNotEmpty - ? _listValidator.getValidatorNameViewState(newNameRequest) + ? _listValidator.getValidatorNameViewState(value) : Right(VerifyNameViewState()); } } \ No newline at end of file diff --git a/lib/features/mailbox_creator/domain/model/verification/duplicate_name_validator.dart b/lib/features/mailbox_creator/domain/model/verification/duplicate_name_validator.dart index 5c48b33400..517b44a0d7 100644 --- a/lib/features/mailbox_creator/domain/model/verification/duplicate_name_validator.dart +++ b/lib/features/mailbox_creator/domain/model/verification/duplicate_name_validator.dart @@ -11,11 +11,11 @@ class DuplicateNameValidator extends Validator { DuplicateNameValidator(this._listName); @override - Either validate(NewNameRequest newNameRequest) { - if (newNameRequest.value != null) { + Either validate(NewNameRequest value) { + if (value.value != null) { final nameExist = _listName .map((nameItem) => nameItem.toLowerCase()) - .contains(newNameRequest.value!.toLowerCase()); + .contains(value.value!.toLowerCase()); if (nameExist) { return Left(VerifyNameFailure(const DuplicatedNameException())); } else { diff --git a/lib/features/mailbox_creator/domain/model/verification/email_address_validator.dart b/lib/features/mailbox_creator/domain/model/verification/email_address_validator.dart index 2785e23b3d..b44d9561c8 100644 --- a/lib/features/mailbox_creator/domain/model/verification/email_address_validator.dart +++ b/lib/features/mailbox_creator/domain/model/verification/email_address_validator.dart @@ -10,8 +10,8 @@ import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_ class EmailAddressValidator extends Validator { @override - Either validate(NewNameRequest newNameRequest) { - if (newNameRequest.value != null && GetUtils.isEmail(newNameRequest.value!)) { + Either validate(NewNameRequest value) { + if (value.value != null && GetUtils.isEmail(value.value!)) { return Right(VerifyNameViewState()); } else { return Left(VerifyNameFailure(const EmailAddressInvalidException())); diff --git a/lib/features/mailbox_creator/domain/model/verification/empty_name_validator.dart b/lib/features/mailbox_creator/domain/model/verification/empty_name_validator.dart index c0d628763e..9e260ca91a 100644 --- a/lib/features/mailbox_creator/domain/model/verification/empty_name_validator.dart +++ b/lib/features/mailbox_creator/domain/model/verification/empty_name_validator.dart @@ -9,8 +9,8 @@ import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_ class EmptyNameValidator extends Validator { @override - Either validate(NewNameRequest newNameRequest) { - if (newNameRequest.value == null || newNameRequest.value!.isEmpty) { + Either validate(NewNameRequest value) { + if (value.value == null || value.value!.isEmpty) { return Left(VerifyNameFailure(const EmptyNameException())); } else { return Right(VerifyNameViewState()); diff --git a/lib/features/mailbox_creator/domain/model/verification/special_character_validator.dart b/lib/features/mailbox_creator/domain/model/verification/special_character_validator.dart index d53ea32446..fc29c9a946 100644 --- a/lib/features/mailbox_creator/domain/model/verification/special_character_validator.dart +++ b/lib/features/mailbox_creator/domain/model/verification/special_character_validator.dart @@ -10,8 +10,8 @@ import 'package:tmail_ui_user/features/mailbox_creator/domain/state/verify_name_ class SpecialCharacterValidator extends Validator { @override - Either validate(NewNameRequest newNameRequest) { - if (newNameRequest.value != null && newNameRequest.value!.hasSpecialCharactersInName()) { + Either validate(NewNameRequest value) { + if (value.value != null && value.value!.hasSpecialCharactersInName()) { return Left(VerifyNameFailure(const SpecialCharacterException())); } else { return Right(VerifyNameViewState()); diff --git a/lib/features/mailbox_creator/domain/state/verify_name_view_state.dart b/lib/features/mailbox_creator/domain/state/verify_name_view_state.dart index f0b668d15d..cd2fe83364 100644 --- a/lib/features/mailbox_creator/domain/state/verify_name_view_state.dart +++ b/lib/features/mailbox_creator/domain/state/verify_name_view_state.dart @@ -1,17 +1,9 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; -import 'package:core/core.dart'; - -class VerifyNameViewState extends UIState { - VerifyNameViewState(); - - @override - List get props => []; -} +class VerifyNameViewState extends UIState {} class VerifyNameFailure extends FeatureFailure { - final dynamic exception; - VerifyNameFailure(this.exception); - @override - List get props => [exception]; + VerifyNameFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_creator/presentation/mailbox_creator_bindings.dart b/lib/features/mailbox_creator/presentation/mailbox_creator_bindings.dart index c7caed4d9a..00f7a1e34c 100644 --- a/lib/features/mailbox_creator/presentation/mailbox_creator_bindings.dart +++ b/lib/features/mailbox_creator/presentation/mailbox_creator_bindings.dart @@ -1,33 +1,12 @@ import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_controller.dart'; -class MailboxCreatorBindings extends BaseBindings { +class MailboxCreatorBindings extends Bindings { @override - void bindingsController() { - Get.lazyPut(() => MailboxCreatorController(Get.find())); - } - - @override - void bindingsDataSource() {} - - @override - void bindingsDataSourceImpl() {} - - @override - void bindingsInteractor() { + void dependencies() { Get.lazyPut(() => VerifyNameInteractor()); - } - - @override - void bindingsRepository() {} - - @override - void bindingsRepositoryImpl() {} - - void dispose() { - Get.delete(); + Get.lazyPut(() => MailboxCreatorController(Get.find())); } } \ No newline at end of file diff --git a/lib/features/mailbox_creator/presentation/mailbox_creator_controller.dart b/lib/features/mailbox_creator/presentation/mailbox_creator_controller.dart index e474f36385..3c6ca30614 100644 --- a/lib/features/mailbox_creator/presentation/mailbox_creator_controller.dart +++ b/lib/features/mailbox_creator/presentation/mailbox_creator_controller.dart @@ -19,19 +19,19 @@ import 'package:tmail_ui_user/features/mailbox_creator/presentation/extensions/v import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/new_mailbox_arguments.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -typedef OnCreatedMailboxCallback = Function(NewMailboxArguments? arguments); - class MailboxCreatorController extends BaseController { final VerifyNameInteractor _verifyNameInteractor; final selectedMailbox = Rxn(); final newNameMailbox = Rxn(); + bool _createdMailbox = false; - FocusNode? nameInputFocusNode; - TextEditingController? nameInputController; + final FocusNode nameInputFocusNode = FocusNode(); + final TextEditingController nameInputController = TextEditingController(); MailboxCreatorArguments? arguments; AccountId? accountId; @@ -39,8 +39,6 @@ class MailboxCreatorController extends BaseController { MailboxTree? defaultMailboxTree; MailboxTree? personalMailboxTree; MailboxTree? teamMailboxesTre; - OnCreatedMailboxCallback? onCreatedMailboxCallback; - VoidCallback? onDismissMailboxCreator; List listMailboxNameAsStringExist = []; @@ -51,45 +49,35 @@ class MailboxCreatorController extends BaseController { @override void onInit() { super.onInit(); - nameInputFocusNode = FocusNode(); - nameInputController = TextEditingController(); + log('MailboxCreatorController::onInit():arguments: ${Get.arguments}'); + arguments = Get.arguments; } @override void onReady() { super.onReady(); + log('MailboxCreatorController::onReady():'); if (arguments != null) { personalMailboxTree = arguments!.personalMailboxTree; defaultMailboxTree = arguments!.defaultMailboxTree; teamMailboxesTre = arguments!.teamMailboxesTree; accountId = arguments!.accountId; _session = arguments!.session; + if (arguments!.selectedMailbox != null) { + selectedMailbox.value = arguments!.selectedMailbox; + } _createListMailboxNameAsStringInMailboxLocation(); } } - @override - void onDone() {} - @override void onClose() { - _disposeWidget(); + log('MailboxCreatorController::onClose():'); + nameInputFocusNode.dispose(); + nameInputController.dispose(); super.onClose(); } - bool isCreateMailboxValidated(BuildContext context) { - final nameValidated = getErrorInputNameString(context); - - if (nameInputFocusNode?.hasFocus == false && newNameMailbox.value == null) { - return false; - } - - if (nameValidated?.isNotEmpty == true) { - return false; - } - return true; - } - MailboxNode? _findMailboxNodeById(MailboxId mailboxId) { final mailboxNode = defaultMailboxTree?.findNode((node) => node.item.id == mailboxId) ?? personalMailboxTree?.findNode((node) => node.item.id == mailboxId) @@ -126,20 +114,19 @@ class MailboxCreatorController extends BaseController { String? getErrorInputNameString(BuildContext context) { final nameMailbox = newNameMailbox.value; - - if (nameInputFocusNode?.hasFocus == false && nameMailbox == null) { - return null; - } + final canCheckNameString = _createdMailbox && nameInputFocusNode.hasFocus == false; return _verifyNameInteractor.execute( nameMailbox, [ - EmptyNameValidator(), + if (canCheckNameString) + EmptyNameValidator(), DuplicateNameValidator(listMailboxNameAsStringExist), ] ).fold( (failure) { if (failure is VerifyNameFailure) { + _createdMailbox = false; return failure.getMessage(context); } else { return null; @@ -150,77 +137,50 @@ class MailboxCreatorController extends BaseController { } void selectMailboxLocation(BuildContext context) async { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); if (accountId != null) { final arguments = DestinationPickerArguments( - accountId!, - MailboxActions.create, - _session, - mailboxIdSelected: selectedMailbox.value?.id); - - if (BuildUtils.isWeb) { - showDialogDestinationPicker( - context: context, - arguments: arguments, - onSelectedMailbox: (destinationMailbox) { - final mailboxDestination = destinationMailbox == PresentationMailbox.unifiedMailbox - ? null - : destinationMailbox; - - selectedMailbox.value = mailboxDestination; - _createListMailboxNameAsStringInMailboxLocation(); - }); - } else { - final destinationMailbox = await push( - AppRoutes.destinationPicker, - arguments: arguments); - - if (destinationMailbox is PresentationMailbox) { - final mailboxDestination = destinationMailbox == PresentationMailbox.unifiedMailbox - ? null - : destinationMailbox; - - selectedMailbox.value = mailboxDestination; - _createListMailboxNameAsStringInMailboxLocation(); - } + accountId!, + MailboxActions.create, + _session, + mailboxIdSelected: selectedMailbox.value?.id); + + final destinationMailbox = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments) + : await push(AppRoutes.destinationPicker, arguments: arguments); + + if (destinationMailbox is PresentationMailbox) { + final mailboxDestination = destinationMailbox == PresentationMailbox.unifiedMailbox + ? null + : destinationMailbox; + + selectedMailbox.value = mailboxDestination; + _createListMailboxNameAsStringInMailboxLocation(); } } } - void _disposeWidget() { - nameInputFocusNode?.dispose(); - nameInputFocusNode = null; - nameInputController?.dispose(); - nameInputController = null; - } - void createNewMailbox(BuildContext context) { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); final nameMailbox = newNameMailbox.value; - if (nameMailbox != null && nameMailbox.isNotEmpty) { - final newMailboxArguments = NewMailboxArguments( - MailboxName(nameMailbox), - mailboxLocation: selectedMailbox.value); + final nameValidated = getErrorInputNameString(context); - if (BuildUtils.isWeb) { - _disposeWidget(); - onCreatedMailboxCallback?.call(newMailboxArguments); - } else { - popBack(result: newMailboxArguments); - } + if (nameValidated == null) { + _createdMailbox = true; + } + + if (nameMailbox != null && nameMailbox.isNotEmpty && _createdMailbox) { + final newMailboxArguments = NewMailboxArguments( + MailboxName(nameMailbox), + mailboxLocation: selectedMailbox.value); + popBack(result: newMailboxArguments); } } void closeMailboxCreator(BuildContext context) { - FocusScope.of(context).unfocus(); - - if (BuildUtils.isWeb) { - _disposeWidget(); - onDismissMailboxCreator?.call(); - } else { - popBack(); - } + KeyboardUtils.hideKeyboard(context); + popBack(); } } \ No newline at end of file diff --git a/lib/features/mailbox_creator/presentation/mailbox_creator_view.dart b/lib/features/mailbox_creator/presentation/mailbox_creator_view.dart index 91f97e0358..ac94dd5061 100644 --- a/lib/features/mailbox_creator/presentation/mailbox_creator_view.dart +++ b/lib/features/mailbox_creator/presentation/mailbox_creator_view.dart @@ -3,14 +3,15 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/text/text_field_builder.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_controller.dart'; -import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/widgets/app_bar_mailbox_creator_builder.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/widgets/create_mailbox_name_input_decoration_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -24,23 +25,11 @@ class MailboxCreatorView extends GetWidget { @override final controller = Get.find(); - MailboxCreatorView({Key? key}) : super(key: key) { - controller.arguments = Get.arguments; - } - - MailboxCreatorView.fromArguments( - MailboxCreatorArguments arguments, { - Key? key, - OnCreatedMailboxCallback? onCreatedMailboxCallback, - VoidCallback? onDismissCallback - }) : super(key: key) { - controller.arguments = arguments; - controller.onCreatedMailboxCallback = onCreatedMailboxCallback; - controller.onDismissMailboxCreator = onDismissCallback; - } + MailboxCreatorView({super.key}); @override Widget build(BuildContext context) { + log('MailboxCreatorView::build():'); return PointerInterceptor( child: GestureDetector( onTap: () => controller.closeMailboxCreator(context), @@ -49,7 +38,7 @@ class MailboxCreatorView extends GetWidget { borderOnForeground: false, color: Colors.transparent, child: SafeArea( - top: !BuildUtils.isWeb && _responsiveUtils.isPortraitMobile(context), + top: PlatformInfo.isMobile && _responsiveUtils.isPortraitMobile(context), bottom: false, left: false, right: false, @@ -80,8 +69,8 @@ class MailboxCreatorView extends GetWidget { child: SafeArea( top: false, bottom: false, - left: !BuildUtils.isWeb && _responsiveUtils.isLandscapeMobile(context), - right: !BuildUtils.isWeb && _responsiveUtils.isLandscapeMobile(context), + left: PlatformInfo.isMobile && _responsiveUtils.isLandscapeMobile(context), + right: PlatformInfo.isMobile && _responsiveUtils.isLandscapeMobile(context), child: Column(children: [ _buildAppBar(context), const Divider(color: AppColor.colorDividerDestinationPicker, height: 1), @@ -100,35 +89,35 @@ class MailboxCreatorView extends GetWidget { } Widget _buildAppBar(BuildContext context) { - return Obx(() => (AppBarMailboxCreatorBuilder( - context, - title: AppLocalizations.of(context).new_mailbox, - isValidated: controller.isCreateMailboxValidated(context)) - ..addOnCancelActionClick(() => controller.closeMailboxCreator(context)) - ..addOnDoneActionClick(() => controller.createNewMailbox(context))) - .build()); + return (AppBarMailboxCreatorBuilder( + context, + title: AppLocalizations.of(context).new_mailbox, + isValidated: true) + ..addOnCancelActionClick(() => controller.closeMailboxCreator(context)) + ..addOnDoneActionClick(() => controller.createNewMailbox(context))) + .build(); } Widget _buildCreateMailboxNameInput(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), - child: Obx(() => (TextFieldBuilder() - ..key(const Key('create_mailbox_name_input')) - ..onChange((value) => controller.setNewNameMailbox(value)) - ..keyboardType(TextInputType.visiblePassword) - ..cursorColor(AppColor.colorTextButton) - ..addController(controller.nameInputController) - ..maxLines(1) - ..textStyle(const TextStyle( - color: AppColor.colorNameEmail, - fontSize: 16, - overflow: CommonTextStyle.defaultTextOverFlow)) - ..addFocusNode(controller.nameInputFocusNode) - ..textDecoration((CreateMailboxNameInputDecorationBuilder() - ..setHintText(AppLocalizations.of(context).hint_input_create_new_mailbox) - ..setErrorText(controller.getErrorInputNameString(context))) - .build())) - .build()) + child: Obx(() => TextFieldBuilder( + onTextChange: controller.setNewNameMailbox, + keyboardType: TextInputType.visiblePassword, + cursorColor: AppColor.colorTextButton, + controller: controller.nameInputController, + textDirection: DirectionUtils.getDirectionByLanguage(context), + maxLines: 1, + textStyle: const TextStyle( + color: AppColor.colorNameEmail, + fontSize: 16, + overflow: CommonTextStyle.defaultTextOverFlow), + focusNode: controller.nameInputFocusNode, + decoration: (CreateMailboxNameInputDecorationBuilder() + ..setHintText(AppLocalizations.of(context).hint_input_create_new_mailbox) + ..setErrorText(controller.getErrorInputNameString(context))) + .build(), + )) ); } @@ -160,12 +149,12 @@ class MailboxCreatorView extends GetWidget { const SizedBox(width: 12), Obx(() => SvgPicture.asset( controller.selectedMailbox.value?.getMailboxIcon(_imagePaths) ?? _imagePaths.icFolderMailbox, - width: BuildUtils.isWeb ? 20 : 24, - height: BuildUtils.isWeb ? 20 : 24, + width: PlatformInfo.isWeb ? 20 : 24, + height: PlatformInfo.isWeb ? 20 : 24, fit: BoxFit.fill)), const SizedBox(width: 12), Expanded(child: Obx(() => Text( - controller.selectedMailbox.value?.name?.name ?? AppLocalizations.of(context).allMailboxes, + controller.selectedMailbox.value?.getDisplayName(context) ?? AppLocalizations.of(context).allMailboxes, maxLines: 1, softWrap: CommonTextStyle.defaultSoftWrap, overflow: CommonTextStyle.defaultTextOverFlow, @@ -179,8 +168,8 @@ class MailboxCreatorView extends GetWidget { IconButton( color: AppColor.primaryColor, icon: SvgPicture.asset( - _imagePaths.icCollapseFolder, - color: AppColor.colorCollapseMailbox, + DirectionUtils.isDirectionRTLByLanguage(context) ? _imagePaths.icBack : _imagePaths.icCollapseFolder, + colorFilter: AppColor.colorCollapseMailbox.asFilter(), fit: BoxFit.fill), onPressed: () => controller.selectMailboxLocation(context)) ])), @@ -190,7 +179,7 @@ class MailboxCreatorView extends GetWidget { } double _getWidthView(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (_responsiveUtils.isMobile(context)) { return double.infinity; } else { @@ -207,7 +196,7 @@ class MailboxCreatorView extends GetWidget { } double _getHeightView(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (_responsiveUtils.isMobile(context)) { return double.infinity; } else { @@ -233,7 +222,7 @@ class MailboxCreatorView extends GetWidget { } EdgeInsets _getMarginView(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (_responsiveUtils.isMobile(context)) { return EdgeInsets.zero; } else { @@ -258,7 +247,7 @@ class MailboxCreatorView extends GetWidget { } BorderRadius _getRadiusView(BuildContext context) { - if (!BuildUtils.isWeb && _responsiveUtils.isLandscapeMobile(context)) { + if (PlatformInfo.isMobile && _responsiveUtils.isLandscapeMobile(context)) { return BorderRadius.zero; } else if (_responsiveUtils.isMobile(context)) { return const BorderRadius.only( diff --git a/lib/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart b/lib/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart index 63ed9d4664..9a34473cd7 100644 --- a/lib/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart +++ b/lib/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree.dart'; class MailboxCreatorArguments with EquatableMixin{ @@ -10,13 +11,16 @@ class MailboxCreatorArguments with EquatableMixin{ final MailboxTree defaultMailboxTree; final MailboxTree teamMailboxesTree; final Session session; + final PresentationMailbox? selectedMailbox; MailboxCreatorArguments( this.accountId, this.defaultMailboxTree, this.personalMailboxTree, this.teamMailboxesTree, - this.session); + this.session, + this.selectedMailbox + ); @override List get props => [ @@ -24,5 +28,7 @@ class MailboxCreatorArguments with EquatableMixin{ defaultMailboxTree, personalMailboxTree, teamMailboxesTree, - session]; + session, + selectedMailbox + ]; } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart b/lib/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart index 1319a40220..a570ccb960 100644 --- a/lib/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart +++ b/lib/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart @@ -1,5 +1,8 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/unread_spam_emails_response.dart'; @@ -12,6 +15,7 @@ abstract class SpamReportDataSource { Future deleteLastTimeDismissedSpamReported(); Future findNumberOfUnreadSpamEmails( + Session session, AccountId accountId, { MailboxFilterCondition? mailboxFilterCondition, @@ -24,4 +28,6 @@ abstract class SpamReportDataSource { Future storeSpamReportState(SpamReportState spamReportState); Future deleteSpamReportState(); + + Future getSpamMailboxCached(AccountId accountId, UserName userName); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/data/datasource_impl/search_datasource_impl.dart b/lib/features/mailbox_dashboard/data/datasource_impl/search_datasource_impl.dart index d32d4bd35b..1c7a6a9c5c 100644 --- a/lib/features/mailbox_dashboard/data/datasource_impl/search_datasource_impl.dart +++ b/lib/features/mailbox_dashboard/data/datasource_impl/search_datasource_impl.dart @@ -1,5 +1,5 @@ -import 'package:tmail_ui_user/features/caching/recent_search_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_search_cache_client.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/search_datasource.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/recent_search_cache.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/extensions/list_recent_search_extension.dart'; @@ -25,9 +25,7 @@ class SearchDataSourceImpl extends SearchDataSource { recentSearch.value, recentSearch.toRecentSearchCache()); } - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override @@ -47,9 +45,7 @@ class SearchDataSourceImpl extends SearchDataSource { : listRecentSearch; return newListRecentSearch; - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } bool _filterRecentSearchCache(RecentSearchCache recentSearchCache, String? pattern) { diff --git a/lib/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart b/lib/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart index eb55124b6a..1b01fd42db 100644 --- a/lib/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart +++ b/lib/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; @@ -11,13 +12,9 @@ class SessionStorageComposerDatasourceImpl @override ComposerCache getComposerCacheOnWeb() { try { - final result = html.window.sessionStorage.entries - .where((e) => e.key == EmailActionType.edit.name) - .toList(); - if (result.isNotEmpty) { - final jsonHandle = - json.decode(result.first.value) as Map; - final emailCache = ComposerCache.fromJson(jsonHandle); + final result = html.window.sessionStorage.entries.firstWhereOrNull((e) => e.key == EmailActionType.reopenComposerBrowser.name); + if (result != null) { + final emailCache = ComposerCache.fromJson(jsonDecode(result.value)); return emailCache; } else { throw NotFoundInWebSessionException(); @@ -30,8 +27,7 @@ class SessionStorageComposerDatasourceImpl @override void removeComposerCacheOnWeb() { try { - html.window.sessionStorage - .removeWhere((key, value) => key == EmailActionType.edit.name); + html.window.sessionStorage.removeWhere((key, value) => key == EmailActionType.reopenComposerBrowser.name); } catch (e) { throw NotFoundInWebSessionException(errorMessage: e.toString()); } @@ -41,7 +37,7 @@ class SessionStorageComposerDatasourceImpl void saveComposerCacheOnWeb(Email email) { try { Map entries = { - EmailActionType.edit.name: json.encode(email.toJson()) + EmailActionType.reopenComposerBrowser.name: email.asString() }; html.window.sessionStorage.addAll(entries); } catch (e) { diff --git a/lib/features/mailbox_dashboard/data/datasource_impl/share_preference_spam_report_data_source_impl.dart b/lib/features/mailbox_dashboard/data/datasource_impl/share_preference_spam_report_data_source_impl.dart index 24f839d438..990d3cd487 100644 --- a/lib/features/mailbox_dashboard/data/datasource_impl/share_preference_spam_report_data_source_impl.dart +++ b/lib/features/mailbox_dashboard/data/datasource_impl/share_preference_spam_report_data_source_impl.dart @@ -1,5 +1,8 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/local/share_preference_spam_report_data_source.dart'; @@ -12,35 +15,31 @@ class SharePreferenceSpamReportDataSourceImpl extends SpamReportDataSource { final ExceptionThrower _exceptionThrower; SharePreferenceSpamReportDataSourceImpl(this._sharePreferenceSpamReportDataSource, this._exceptionThrower); + @override Future getLastTimeDismissedSpamReported() async { return Future.sync(() async { return await _sharePreferenceSpamReportDataSource.getLastTimeDismissedSpamReported(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future storeLastTimeDismissedSpamReported(DateTime lastTimeDismissedSpamReported) async { return Future.sync(() async { return await _sharePreferenceSpamReportDataSource.storeLastTimeDismissedSpamReported(lastTimeDismissedSpamReported); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future deleteLastTimeDismissedSpamReported() { return Future.sync(() async { return await _sharePreferenceSpamReportDataSource.deleteLastTimeDismissedSpamReported(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future findNumberOfUnreadSpamEmails( + Session session, AccountId accountId, { MailboxFilterCondition? mailboxFilterCondition, @@ -54,26 +53,25 @@ class SharePreferenceSpamReportDataSourceImpl extends SpamReportDataSource { Future deleteSpamReportState() { return Future.sync(() async { return await _sharePreferenceSpamReportDataSource.deleteLastTimeDismissedSpamReported(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future getSpamReportState() { return Future.sync(() async { return await _sharePreferenceSpamReportDataSource.getSpamReportState(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future storeSpamReportState(SpamReportState spamReportState) { return Future.sync(() async { return await _sharePreferenceSpamReportDataSource.storeSpamReportState(spamReportState); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future getSpamMailboxCached(AccountId accountId, UserName userName) { + throw UnimplementedError(); } } diff --git a/lib/features/mailbox_dashboard/data/datasource_impl/spam_report_cache_datasource_impl.dart b/lib/features/mailbox_dashboard/data/datasource_impl/spam_report_cache_datasource_impl.dart new file mode 100644 index 0000000000..e9a685b83b --- /dev/null +++ b/lib/features/mailbox_dashboard/data/datasource_impl/spam_report_cache_datasource_impl.dart @@ -0,0 +1,61 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; +import 'package:tmail_ui_user/features/mailbox/data/local/mailbox_cache_manager.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/unread_spam_emails_response.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; + +class SpamReportCacheDataSourceImpl extends SpamReportDataSource { + final MailboxCacheManager _mailboxCacheManager; + final ExceptionThrower _exceptionThrower; + + SpamReportCacheDataSourceImpl(this._mailboxCacheManager, this._exceptionThrower); + + @override + Future deleteLastTimeDismissedSpamReported() { + throw UnimplementedError(); + } + + @override + Future deleteSpamReportState() { + throw UnimplementedError(); + } + + @override + Future findNumberOfUnreadSpamEmails(Session session, AccountId accountId, {MailboxFilterCondition? mailboxFilterCondition, UnsignedInt? limit}) { + throw UnimplementedError(); + } + + @override + Future getLastTimeDismissedSpamReported() { + throw UnimplementedError(); + } + + @override + Future getSpamMailboxCached(AccountId accountId, UserName userName) { + return Future.sync(() async { + return await _mailboxCacheManager.getSpamMailbox(accountId, userName); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future getSpamReportState() { + throw UnimplementedError(); + } + + @override + Future storeLastTimeDismissedSpamReported(DateTime lastTimeDismissedSpamReported) { + throw UnimplementedError(); + } + + @override + Future storeSpamReportState(SpamReportState spamReportState) { + throw UnimplementedError(); + } +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/data/datasource_impl/spam_report_datasource_impl.dart b/lib/features/mailbox_dashboard/data/datasource_impl/spam_report_datasource_impl.dart index 545cf52664..d57d9799da 100644 --- a/lib/features/mailbox_dashboard/data/datasource_impl/spam_report_datasource_impl.dart +++ b/lib/features/mailbox_dashboard/data/datasource_impl/spam_report_datasource_impl.dart @@ -1,6 +1,9 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/network/spam_report_api.dart'; @@ -21,6 +24,7 @@ class SpamReportDataSourceImpl extends SpamReportDataSource { @override Future findNumberOfUnreadSpamEmails( + Session session, AccountId accountId, { MailboxFilterCondition? mailboxFilterCondition, @@ -28,12 +32,13 @@ class SpamReportDataSourceImpl extends SpamReportDataSource { } ) { return Future.sync(() async { - final _unreadSpamEmailsResponse = await _spamReportApi.getUnreadSpamEmailbox( - accountId, mailboxFilterCondition: mailboxFilterCondition, limit: limit); - return _unreadSpamEmailsResponse; - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + final unreadSpamEmailsResponse = await _spamReportApi.getUnreadSpamEmailbox( + session, + accountId, + mailboxFilterCondition: mailboxFilterCondition, + limit: limit); + return unreadSpamEmailsResponse; + }).catchError(_exceptionThrower.throwException); } @override @@ -61,4 +66,8 @@ class SpamReportDataSourceImpl extends SpamReportDataSource { throw UnimplementedError(); } + @override + Future getSpamMailboxCached(AccountId accountId, UserName userName) { + throw UnimplementedError(); + } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/data/local/share_preference_spam_report_data_source.dart b/lib/features/mailbox_dashboard/data/local/share_preference_spam_report_data_source.dart index 9f80ce4a5c..ac36be8e99 100644 --- a/lib/features/mailbox_dashboard/data/local/share_preference_spam_report_data_source.dart +++ b/lib/features/mailbox_dashboard/data/local/share_preference_spam_report_data_source.dart @@ -1,11 +1,14 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/unread_spam_emails_response.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/utils/mailbox_dashboard_constant.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/utils/mailbox_dashboard_constant.dart'; class SharePreferenceSpamReportDataSource extends SpamReportDataSource { final SharedPreferences _sharedPreferences; @@ -14,15 +17,15 @@ class SharePreferenceSpamReportDataSource extends SpamReportDataSource { @override Future getLastTimeDismissedSpamReported() async { - final _timeStamp = _sharedPreferences.getInt(MailboxDashboardConstant.keyLastTimeDismissedSpamReported) ?? 0; - final _lastTimeDismissedSpamReported = DateTime.fromMillisecondsSinceEpoch(_timeStamp); - return _lastTimeDismissedSpamReported; + final timeStamp = _sharedPreferences.getInt(MailboxDashboardConstant.keyLastTimeDismissedSpamReported) ?? 0; + final lastTimeDismissedSpamReported = DateTime.fromMillisecondsSinceEpoch(timeStamp); + return lastTimeDismissedSpamReported; } @override Future storeLastTimeDismissedSpamReported(DateTime lastTimeDismissedSpamReported) async { - final _timeStamp = lastTimeDismissedSpamReported.millisecondsSinceEpoch; - return await _sharedPreferences.setInt(MailboxDashboardConstant.keyLastTimeDismissedSpamReported,_timeStamp); + final timeStamp = lastTimeDismissedSpamReported.millisecondsSinceEpoch; + return await _sharedPreferences.setInt(MailboxDashboardConstant.keyLastTimeDismissedSpamReported,timeStamp); } @override @@ -32,6 +35,7 @@ class SharePreferenceSpamReportDataSource extends SpamReportDataSource { @override Future findNumberOfUnreadSpamEmails( + Session session, AccountId accountId, { MailboxFilterCondition? mailboxFilterCondition, @@ -48,13 +52,18 @@ class SharePreferenceSpamReportDataSource extends SpamReportDataSource { @override Future getSpamReportState() async { - final _spamReportState = _sharedPreferences.getString(MailboxDashboardConstant.keySpamReportState) ?? ''; - return _spamReportState == SpamReportState.disabled.keyValue ? SpamReportState.disabled : SpamReportState.enabled; + final spamReportState = _sharedPreferences.getString(MailboxDashboardConstant.keySpamReportState) ?? ''; + return spamReportState == SpamReportState.disabled.keyValue ? SpamReportState.disabled : SpamReportState.enabled; } @override Future storeSpamReportState(SpamReportState spamReportState) async { - final _spamReportState = spamReportState.keyValue; - return await _sharedPreferences.setString(MailboxDashboardConstant.keySpamReportState, _spamReportState); + final spamReportState0 = spamReportState.keyValue; + return await _sharedPreferences.setString(MailboxDashboardConstant.keySpamReportState, spamReportState0); + } + + @override + Future getSpamMailboxCached(AccountId accountId, UserName userName) { + throw UnimplementedError(); } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/data/model/composer_cache.dart b/lib/features/mailbox_dashboard/data/model/composer_cache.dart index bbe03218c8..5c5f290259 100644 --- a/lib/features/mailbox_dashboard/data/model/composer_cache.dart +++ b/lib/features/mailbox_dashboard/data/model/composer_cache.dart @@ -8,7 +8,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/email_content.dart'; -import 'package:model/extensions/media_type_extension.dart'; +import 'package:model/extensions/media_type_nullable_extension.dart'; class ComposerCache with EquatableMixin { diff --git a/lib/features/mailbox_dashboard/data/network/spam_report_api.dart b/lib/features/mailbox_dashboard/data/network/spam_report_api.dart index ff536b83f5..3c0244b79f 100644 --- a/lib/features/mailbox_dashboard/data/network/spam_report_api.dart +++ b/lib/features/mailbox_dashboard/data/network/spam_report_api.dart @@ -1,6 +1,7 @@ import 'package:jmap_dart_client/http/http_client.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/request/reference_path.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/jmap_request.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/get/get_mailbox_method.dart'; @@ -8,6 +9,7 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/get/get_mailbox_response.dart import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/query/query_mailbox_method.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/unread_spam_emails_response.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; class SpamReportApi { final HttpClient _httpClient; @@ -16,6 +18,7 @@ class SpamReportApi { SpamReportApi(this._httpClient); Future getUnreadSpamEmailbox( + Session session, AccountId accountId, { MailboxFilterCondition? mailboxFilterCondition, @@ -35,17 +38,21 @@ class SpamReportApi { ReferencePath.idsPath, )); final getMailboxInvocation = requestBuilder.invocation(getMailBoxMethod); + + final capabilities = getMailBoxMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final result = await (requestBuilder - ..usings(getMailBoxMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); - final _mailboxResponse = result + final mailboxResponse = result .parse(getMailboxInvocation.methodCallId, GetMailboxResponse.deserialize); return Future.sync(() async { - final _unreadSpamMailbox = _mailboxResponse?.list.first; - return UnreadSpamEmailsResponse(unreadSpamMailbox: _unreadSpamMailbox); + final unreadSpamMailbox = mailboxResponse?.list.first; + return UnreadSpamEmailsResponse(unreadSpamMailbox: unreadSpamMailbox); }).catchError((error) { throw error; }); diff --git a/lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart b/lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart index e62408ac48..d1732e7e41 100644 --- a/lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart +++ b/lib/features/mailbox_dashboard/data/repository/spam_report_repository_impl.dart @@ -1,6 +1,9 @@ import 'package:core/data/model/source_type/data_source_type.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; @@ -29,6 +32,7 @@ class SpamReportRepositoryImpl extends SpamReportRepository { @override Future getUnreadSpamMailbox( + Session session, AccountId accountId, { MailboxFilterCondition? mailboxFilterCondition, @@ -36,9 +40,10 @@ class SpamReportRepositoryImpl extends SpamReportRepository { } ) { return mapDataSource[DataSourceType.network]!.findNumberOfUnreadSpamEmails( - accountId, - mailboxFilterCondition: mailboxFilterCondition, - limit: limit); + session, + accountId, + mailboxFilterCondition: mailboxFilterCondition, + limit: limit); } @override @@ -55,4 +60,9 @@ class SpamReportRepositoryImpl extends SpamReportRepository { Future deleteSpamReportState() { return mapDataSource[DataSourceType.local]!.deleteSpamReportState(); } + + @override + Future getSpamMailboxCached(AccountId accountId, UserName userName) { + return mapDataSource[DataSourceType.hiveCache]!.getSpamMailboxCached(accountId, userName); + } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/app_dashboard/linagora_app.dart b/lib/features/mailbox_dashboard/domain/app_dashboard/linagora_app.dart index 8624b6d407..29484fd4a1 100644 --- a/lib/features/mailbox_dashboard/domain/app_dashboard/linagora_app.dart +++ b/lib/features/mailbox_dashboard/domain/app_dashboard/linagora_app.dart @@ -3,7 +3,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'linagora_app.g.dart'; -@JsonSerializable() +@JsonSerializable(explicitToJson: true, includeIfNull: false) class LinagoraApp with EquatableMixin{ @JsonKey(name: 'appName') final String appName; @@ -14,12 +14,32 @@ class LinagoraApp with EquatableMixin{ @JsonKey(name: 'appLink') final Uri appUri; - LinagoraApp(this.appName, this.iconName, this.appUri); + final String? androidPackageId; + final String? iosUrlScheme; + final String? iosAppStoreLink; + + LinagoraApp( + this.appName, + this.iconName, + this.appUri, + { + this.androidPackageId, + this.iosUrlScheme, + this.iosAppStoreLink + } + ); factory LinagoraApp.fromJson(Map json) => _$LinagoraAppFromJson(json); Map toJson() => _$LinagoraAppToJson(this); @override - List get props => [appName, iconName, appUri]; + List get props => [ + appName, + iconName, + appUri, + androidPackageId, + iosUrlScheme, + iosAppStoreLink + ]; } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart b/lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart index 344665221b..81401e11ab 100644 --- a/lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart +++ b/lib/features/mailbox_dashboard/domain/exceptions/spam_report_exception.dart @@ -1,2 +1,4 @@ -class NotFoundLastTimeDismissedSpamReportException implements Exception {} \ No newline at end of file +class NotFoundLastTimeDismissedSpamReportException implements Exception {} + +class NotFoundSpamMailboxCachedException implements Exception {} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart b/lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart index f814a0fa04..b1e17290b4 100644 --- a/lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart +++ b/lib/features/mailbox_dashboard/domain/repository/spam_report_repository.dart @@ -1,5 +1,8 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/unread_spam_emails_response.dart'; @@ -12,6 +15,7 @@ abstract class SpamReportRepository { Future deleteLastTimeDismissedSpamReported(); Future getUnreadSpamMailbox( + Session session, AccountId accountId, { MailboxFilterCondition? mailboxFilterCondition, @@ -24,4 +28,6 @@ abstract class SpamReportRepository { Future storeSpamReportState(SpamReportState spamReportState); Future deleteSpamReportState(); + + Future getSpamMailboxCached(AccountId accountId, UserName userName); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/delete_last_time_dismissed_spam_reported_state.dart b/lib/features/mailbox_dashboard/domain/state/delete_last_time_dismissed_spam_reported_state.dart index cdf2d116db..7b26f6524e 100644 --- a/lib/features/mailbox_dashboard/domain/state/delete_last_time_dismissed_spam_reported_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/delete_last_time_dismissed_spam_reported_state.dart @@ -3,19 +3,9 @@ import 'package:core/presentation/state/success.dart'; class DeleteLastTimeDismissedSpamReportedLoading extends UIState {} -class DeleteLastTimeDismissedSpamReportedSuccess extends UIState { - - DeleteLastTimeDismissedSpamReportedSuccess(); - - @override - List get props => []; -} +class DeleteLastTimeDismissedSpamReportedSuccess extends UIState {} class DeleteLastTimeDismissedSpamReportedFailure extends FeatureFailure { - final dynamic exception; - - DeleteLastTimeDismissedSpamReportedFailure(this.exception); - @override - List get props => [exception]; + DeleteLastTimeDismissedSpamReportedFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/delete_spam_report_state.dart b/lib/features/mailbox_dashboard/domain/state/delete_spam_report_state.dart index 7f40ce636e..dc14e23398 100644 --- a/lib/features/mailbox_dashboard/domain/state/delete_spam_report_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/delete_spam_report_state.dart @@ -3,19 +3,9 @@ import 'package:core/presentation/state/success.dart'; class DeleteSpamReportStateLoading extends UIState {} -class DeleteSpamReportStateSuccess extends UIState { - - DeleteSpamReportStateSuccess(); - - @override - List get props => []; -} +class DeleteSpamReportStateSuccess extends UIState {} class DeleteSpamReportStateFailure extends FeatureFailure { - final dynamic exception; - - DeleteSpamReportStateFailure(this.exception); - @override - List get props => [exception]; + DeleteSpamReportStateFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/get_all_recent_search_latest_state.dart b/lib/features/mailbox_dashboard/domain/state/get_all_recent_search_latest_state.dart index 2394bc574d..db14c3f964 100644 --- a/lib/features/mailbox_dashboard/domain/state/get_all_recent_search_latest_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/get_all_recent_search_latest_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart'; class GetAllRecentSearchLatestSuccess extends UIState { @@ -12,10 +13,6 @@ class GetAllRecentSearchLatestSuccess extends UIState { } class GetAllRecentSearchLatestFailure extends FeatureFailure { - final dynamic exception; - GetAllRecentSearchLatestFailure(this.exception); - - @override - List get props => [exception]; + GetAllRecentSearchLatestFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/get_app_dashboard_configuration_state.dart b/lib/features/mailbox_dashboard/domain/state/get_app_dashboard_configuration_state.dart index 73316bbfbe..644fc1ff0f 100644 --- a/lib/features/mailbox_dashboard/domain/state/get_app_dashboard_configuration_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/get_app_dashboard_configuration_state.dart @@ -15,10 +15,6 @@ class GetAppDashboardConfigurationSuccess extends UIState { } class GetAppDashboardConfigurationFailure extends FeatureFailure { - final dynamic exception; - GetAppDashboardConfigurationFailure(this.exception); - - @override - List get props => [exception]; + GetAppDashboardConfigurationFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/get_composer_cache_state.dart b/lib/features/mailbox_dashboard/domain/state/get_composer_cache_state.dart index 51ab427364..c02f573593 100644 --- a/lib/features/mailbox_dashboard/domain/state/get_composer_cache_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/get_composer_cache_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/model/composer_cache.dart'; class GetComposerCacheSuccess extends UIState { @@ -12,10 +13,6 @@ class GetComposerCacheSuccess extends UIState { } class GetComposerCacheFailure extends FeatureFailure { - final dynamic exception; - GetComposerCacheFailure(this.exception); - - @override - List get props => [exception]; + GetComposerCacheFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart b/lib/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart index e5d3a19a11..0dceb71418 100644 --- a/lib/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart @@ -13,16 +13,9 @@ class GetUnreadSpamMailboxSuccess extends UIState { List get props => [unreadSpamMailbox]; } -class InvalidSpamReportCondition extends FeatureFailure { - @override - List get props => []; -} +class InvalidSpamReportCondition extends FeatureFailure {} class GetUnreadSpamMailboxFailure extends FeatureFailure { - final dynamic exception; - GetUnreadSpamMailboxFailure(this.exception); - - @override - List get props => [exception]; + GetUnreadSpamMailboxFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart b/lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart new file mode 100644 index 0000000000..d03094bf1a --- /dev/null +++ b/lib/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart @@ -0,0 +1,20 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; + +class GetSpamMailboxCachedLoading extends UIState {} + +class GetSpamMailboxCachedSuccess extends UIState { + + final Mailbox spamMailbox; + + GetSpamMailboxCachedSuccess(this.spamMailbox); + + @override + List get props => [spamMailbox]; +} + +class GetSpamMailboxCachedFailure extends FeatureFailure { + + GetSpamMailboxCachedFailure(exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/get_spam_report_state.dart b/lib/features/mailbox_dashboard/domain/state/get_spam_report_state.dart index 3d03b1708c..2d5b34157d 100644 --- a/lib/features/mailbox_dashboard/domain/state/get_spam_report_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/get_spam_report_state.dart @@ -14,10 +14,6 @@ class GetSpamReportStateSuccess extends UIState { } class GetSpamReportStateFailure extends FeatureFailure { - final dynamic exception; - GetSpamReportStateFailure(this.exception); - - @override - List get props => [exception]; + GetSpamReportStateFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/get_user_profile_state.dart b/lib/features/mailbox_dashboard/domain/state/get_user_profile_state.dart index 2dfa701490..0598dd21c1 100644 --- a/lib/features/mailbox_dashboard/domain/state/get_user_profile_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/get_user_profile_state.dart @@ -1,5 +1,6 @@ -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/user/user_profile.dart'; class GetUserProfileSuccess extends UIState { final UserProfile userProfile; @@ -11,10 +12,6 @@ class GetUserProfileSuccess extends UIState { } class GetUserProfileFailure extends FeatureFailure { - final dynamic exception; - GetUserProfileFailure(this.exception); - - @override - List get props => [exception]; + GetUserProfileFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/quick_search_email_state.dart b/lib/features/mailbox_dashboard/domain/state/quick_search_email_state.dart index 37e933b41a..80573edb4d 100644 --- a/lib/features/mailbox_dashboard/domain/state/quick_search_email_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/quick_search_email_state.dart @@ -1,5 +1,5 @@ - -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:model/email/presentation_email.dart'; class QuickSearchEmailSuccess extends UIState { @@ -12,10 +12,6 @@ class QuickSearchEmailSuccess extends UIState { } class QuickSearchEmailFailure extends FeatureFailure { - final dynamic exception; - - QuickSearchEmailFailure(this.exception); - @override - List get props => [exception]; + QuickSearchEmailFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/remove_composer_cache_state.dart b/lib/features/mailbox_dashboard/domain/state/remove_composer_cache_state.dart index 72349c1d21..04460eda13 100644 --- a/lib/features/mailbox_dashboard/domain/state/remove_composer_cache_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/remove_composer_cache_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; class RemoveComposerCacheSuccess extends UIState { @@ -9,10 +10,6 @@ class RemoveComposerCacheSuccess extends UIState { } class RemoveComposerCacheFailure extends FeatureFailure { - final dynamic exception; - RemoveComposerCacheFailure(this.exception); - - @override - List get props => [exception]; + RemoveComposerCacheFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart b/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart index ffa29e646e..97bf45ddea 100644 --- a/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart @@ -1,4 +1,4 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; @@ -8,16 +8,9 @@ class RemoveEmailDraftsSuccess extends UIActionState { jmap.State? currentEmailState, jmap.State? currentMailboxState, }) : super(currentEmailState, currentMailboxState); - - @override - List get props => []; } class RemoveEmailDraftsFailure extends FeatureFailure { - final dynamic exception; - - RemoveEmailDraftsFailure(this.exception); - @override - List get props => [exception]; + RemoveEmailDraftsFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/save_composer_cache_state.dart b/lib/features/mailbox_dashboard/domain/state/save_composer_cache_state.dart index a2552f667f..c4119537bc 100644 --- a/lib/features/mailbox_dashboard/domain/state/save_composer_cache_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/save_composer_cache_state.dart @@ -1,18 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; -class SaveComposerCacheSuccess extends UIState { - - SaveComposerCacheSuccess(); - - @override - List get props => []; -} +class SaveComposerCacheSuccess extends UIState {} class SaveComposerCacheFailure extends FeatureFailure { - final dynamic exception; - - SaveComposerCacheFailure(this.exception); - @override - List get props => [exception]; + SaveComposerCacheFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/save_recent_search_state.dart b/lib/features/mailbox_dashboard/domain/state/save_recent_search_state.dart index 88a58ee0bd..c78b824cbe 100644 --- a/lib/features/mailbox_dashboard/domain/state/save_recent_search_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/save_recent_search_state.dart @@ -1,18 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; -class SaveRecentSearchSuccess extends UIState { - - SaveRecentSearchSuccess(); - - @override - List get props => []; -} +class SaveRecentSearchSuccess extends UIState {} class SaveRecentSearchFailure extends FeatureFailure { - final dynamic exception; - - SaveRecentSearchFailure(this.exception); - @override - List get props => [exception]; + SaveRecentSearchFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/store_last_time_dismissed_spam_reported_state.dart b/lib/features/mailbox_dashboard/domain/state/store_last_time_dismissed_spam_reported_state.dart index 7a9509984f..5cdfe2ad65 100644 --- a/lib/features/mailbox_dashboard/domain/state/store_last_time_dismissed_spam_reported_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/store_last_time_dismissed_spam_reported_state.dart @@ -3,19 +3,9 @@ import 'package:core/presentation/state/success.dart'; class StoreLastTimeDismissedSpamReportLoading extends UIState {} -class StoreLastTimeDismissedSpamReportSuccess extends UIState { - - StoreLastTimeDismissedSpamReportSuccess(); - - @override - List get props => []; -} +class StoreLastTimeDismissedSpamReportSuccess extends UIState {} class StoreLastTimeDismissedSpamReportFailure extends FeatureFailure { - final dynamic exception; - - StoreLastTimeDismissedSpamReportFailure(this.exception); - @override - List get props => [exception]; + StoreLastTimeDismissedSpamReportFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/state/store_spam_report_state.dart b/lib/features/mailbox_dashboard/domain/state/store_spam_report_state.dart index 318082d88d..5ecaa1da40 100644 --- a/lib/features/mailbox_dashboard/domain/state/store_spam_report_state.dart +++ b/lib/features/mailbox_dashboard/domain/state/store_spam_report_state.dart @@ -10,14 +10,10 @@ class StoreSpamReportStateSuccess extends UIState { StoreSpamReportStateSuccess(this.spamReportState); @override - List get props => []; + List get props => [spamReportState]; } class StoreSpamReportStateFailure extends FeatureFailure { - final dynamic exception; - StoreSpamReportStateFailure(this.exception); - - @override - List get props => [exception]; + StoreSpamReportStateFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart new file mode 100644 index 0000000000..e48bec70a6 --- /dev/null +++ b/lib/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart @@ -0,0 +1,40 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/spam_report_repository.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/utils/mailbox_dashboard_constant.dart'; + +class GetSpamMailboxCachedInteractor { + final SpamReportRepository _spamReportRepository; + + GetSpamMailboxCachedInteractor(this._spamReportRepository); + + Stream> execute(AccountId accountId, UserName userName) async* { + try { + yield Right(GetSpamMailboxCachedLoading()); + + final lastTimeDismissedSpamReported = await _spamReportRepository.getLastTimeDismissedSpamReported(); + final timeLast = DateTime.now().difference(lastTimeDismissedSpamReported); + final checkTimeCondition = timeLast.inHours > MailboxDashboardConstant.spamReportBannerDisplayTimeOut; + + if (checkTimeCondition) { + final spamMailbox = await _spamReportRepository.getSpamMailboxCached(accountId, userName); + final countUnreadSpamMailbox = spamMailbox.unreadEmails?.value.value.toInt() ?? 0; + if (countUnreadSpamMailbox > 0) { + yield Right(GetSpamMailboxCachedSuccess(spamMailbox)); + } else { + yield Left(InvalidSpamReportCondition()); + } + } else { + yield Left(InvalidSpamReportCondition()); + } + } catch (e) { + yield Left(GetSpamMailboxCachedFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/domain/usecases/get_unread_spam_mailbox_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/get_unread_spam_mailbox_interactor.dart index c303407630..23c2596a98 100644 --- a/lib/features/mailbox_dashboard/domain/usecases/get_unread_spam_mailbox_interactor.dart +++ b/lib/features/mailbox_dashboard/domain/usecases/get_unread_spam_mailbox_interactor.dart @@ -3,6 +3,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/spam_report_repository.dart'; @@ -15,6 +16,7 @@ class GetUnreadSpamMailboxInteractor { GetUnreadSpamMailboxInteractor(this._spamReportRepository); Stream> execute( + Session session, AccountId accountId, { UnsignedInt? limit, @@ -23,17 +25,21 @@ class GetUnreadSpamMailboxInteractor { ) async* { try { yield Right(GetUnreadSpamMailboxLoading()); - final _lastTimeDissmissedSpamReported = await _spamReportRepository.getLastTimeDismissedSpamReported(); - final _timeLast = DateTime.now().difference(_lastTimeDissmissedSpamReported); + final lastTimeDismissedSpamReported = await _spamReportRepository.getLastTimeDismissedSpamReported(); + final timeLast = DateTime.now().difference(lastTimeDismissedSpamReported); - final _checkTimeCondition = (_timeLast.inHours > 0) && (_timeLast.inHours > conditionsForDisplayingSpamReportBanner); + final checkTimeCondition = (timeLast.inHours > 0) && (timeLast.inHours > conditionsForDisplayingSpamReportBanner); - if (_checkTimeCondition) { - final _response = await _spamReportRepository.getUnreadSpamMailbox(accountId, mailboxFilterCondition: mailboxFilterCondition, limit: limit); - final _unreadSpamMailbox = _response.unreadSpamMailbox; + if (checkTimeCondition) { + final response = await _spamReportRepository.getUnreadSpamMailbox( + session, + accountId, + mailboxFilterCondition: mailboxFilterCondition, + limit: limit); + final unreadSpamMailbox = response.unreadSpamMailbox; - if (_unreadSpamMailbox!.unreadEmails!.value.value > 0) { - yield Right(GetUnreadSpamMailboxSuccess(_unreadSpamMailbox)); + if (unreadSpamMailbox!.unreadEmails!.value.value > 0) { + yield Right(GetUnreadSpamMailboxSuccess(unreadSpamMailbox)); } else { yield Left(InvalidSpamReportCondition()); } diff --git a/lib/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart index ea1a170c39..6e363114b8 100644 --- a/lib/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart +++ b/lib/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:model/model.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -17,6 +18,7 @@ class QuickSearchEmailInteractor { QuickSearchEmailInteractor(this.threadRepository); Future> execute( + Session session, AccountId accountId, { UnsignedInt? limit, @@ -27,6 +29,7 @@ class QuickSearchEmailInteractor { ) async { try { final emailList = await threadRepository.searchEmails( + session, accountId, limit: limit, sort: sort, diff --git a/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart b/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart index e63f666876..291ef7cd21 100644 --- a/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart +++ b/lib/features/mailbox_dashboard/domain/usecases/remove_email_drafts_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; @@ -12,17 +13,17 @@ class RemoveEmailDraftsInteractor { RemoveEmailDraftsInteractor(this._emailRepository, this._mailboxRepository); - Stream> execute(AccountId accountId, EmailId emailId) async* { + Stream> execute(Session session, AccountId accountId, EmailId emailId) async* { try { final listState = await Future.wait([ - _mailboxRepository.getMailboxState(), - _emailRepository.getEmailState(), + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), ], eagerError: true); final currentMailboxState = listState.first; final currentEmailState = listState.last; - final result = await _emailRepository.removeEmailDrafts(accountId, emailId); + final result = await _emailRepository.removeEmailDrafts(session, accountId, emailId); if (result) { yield Right(RemoveEmailDraftsSuccess( currentEmailState: currentEmailState, diff --git a/lib/features/mailbox_dashboard/utils/mailbox_dashboard_constant.dart b/lib/features/mailbox_dashboard/domain/utils/mailbox_dashboard_constant.dart similarity index 78% rename from lib/features/mailbox_dashboard/utils/mailbox_dashboard_constant.dart rename to lib/features/mailbox_dashboard/domain/utils/mailbox_dashboard_constant.dart index b7ccf67e8d..47c5614f2b 100644 --- a/lib/features/mailbox_dashboard/utils/mailbox_dashboard_constant.dart +++ b/lib/features/mailbox_dashboard/domain/utils/mailbox_dashboard_constant.dart @@ -1,5 +1,6 @@ + class MailboxDashboardConstant { static const String keyLastTimeDismissedSpamReported = 'KEY_LAST_TIME_DISMISSED_SPAM_REPORTED'; - static const String keySpamReportState = 'KEY_SPAM_REPORT_STATE'; + static const int spamReportBannerDisplayTimeOut = 4; } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart index 787beaa8c4..b65c8f55a1 100644 --- a/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart +++ b/lib/features/mailbox_dashboard/presentation/action/dashboard_action.dart @@ -2,8 +2,8 @@ import 'package:flutter/cupertino.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; -import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/main/routes/navigation_router.dart'; @@ -16,29 +16,11 @@ class DashBoardAction extends UIAction { List get props => []; } -class RefreshAllEmailAction extends DashBoardAction { +class RefreshAllEmailAction extends DashBoardAction {} - RefreshAllEmailAction(); +class SelectionAllEmailAction extends DashBoardAction {} - @override - List get props => []; -} - -class SelectionAllEmailAction extends DashBoardAction { - - SelectionAllEmailAction(); - - @override - List get props => []; -} - -class CancelSelectionAllEmailAction extends DashBoardAction { - - CancelSelectionAllEmailAction(); - - @override - List get props => []; -} +class CancelSelectionAllEmailAction extends DashBoardAction {} class FilterMessageAction extends DashBoardAction { @@ -74,13 +56,7 @@ class OpenEmailDetailedFromSuggestionQuickSearchAction extends DashBoardAction { List get props => [presentationEmail]; } -class StartSearchEmailAction extends DashBoardAction { - - StartSearchEmailAction(); - - @override - List get props => []; -} +class StartSearchEmailAction extends DashBoardAction {} class EmptyTrashAction extends DashBoardAction { @@ -92,19 +68,9 @@ class EmptyTrashAction extends DashBoardAction { List get props => []; } -class ClearSearchEmailAction extends DashBoardAction { - ClearSearchEmailAction(); +class ClearSearchEmailAction extends DashBoardAction {} - @override - List get props => []; -} - -class ClearAllFieldOfAdvancedSearchAction extends DashBoardAction { - ClearAllFieldOfAdvancedSearchAction(); - - @override - List get props => []; -} +class ClearAllFieldOfAdvancedSearchAction extends DashBoardAction {} class SelectEmailByIdAction extends DashBoardAction { @@ -125,13 +91,24 @@ class SearchEmailByQueryAction extends DashBoardAction { @override List get props => [navigationRouter]; } -class OpenMailboxAction extends DashBoardAction { - final BuildContext context; - final PresentationMailbox presentationMailbox; +class SelectDateRangeToAdvancedSearch extends DashBoardAction { + + final DateTime? startDate; + final DateTime? endDate; + + SelectDateRangeToAdvancedSearch(this.startDate, this.endDate); + + @override + List get props => [startDate, endDate]; +} + +class ClearDateRangeToAdvancedSearch extends DashBoardAction { + + final EmailReceiveTimeType receiveTime; - OpenMailboxAction(this.context, this.presentationMailbox); + ClearDateRangeToAdvancedSearch(this.receiveTime); @override - List get props => [context, presentationMailbox]; + List get props => [receiveTime]; } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart index 733144919f..f16bae7d48 100644 --- a/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart +++ b/lib/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart @@ -1,15 +1,17 @@ import 'package:core/data/model/source_type/data_source_type.dart'; -import 'package:core/data/network/dio_client.dart'; import 'package:core/utils/config/app_config_loader.dart'; +import 'package:core/utils/file_utils.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/caching/recent_search_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_search_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; import 'package:tmail_ui_user/features/composer/data/repository/contact_repository_impl.dart'; import 'package:tmail_ui_user/features/composer/domain/repository/contact_repository.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart'; import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; @@ -22,25 +24,9 @@ import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_ import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/email_supervisor_controller.dart'; import 'package:tmail_ui_user/features/email/presentation/controller/single_email_controller.dart'; -import 'package:tmail_ui_user/features/email/presentation/email_bindings.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/account_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/hive_account_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; -import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; -import 'package:tmail_ui_user/features/login/data/repository/account_repository_impl.dart'; -import 'package:tmail_ui_user/features/login/data/repository/authentication_oidc_repository_impl.dart'; +import 'package:tmail_ui_user/features/email/presentation/bindings/email_bindings.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/get_credential_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_token_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; @@ -58,9 +44,10 @@ import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_controller.d import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/search_datasource.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/session_storage_composer_datasource.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource/spam_report_datasource.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/share_preference_spam_report_data_source_impl.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/search_datasource_impl.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/session_storage_composer_datasoure_impl.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/share_preference_spam_report_data_source_impl.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/spam_report_cache_datasource_impl.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/datasource_impl/spam_report_datasource_impl.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/local/share_preference_spam_report_data_source.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/network/spam_report_api.dart'; @@ -73,6 +60,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/repository/spam_ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_all_recent_search_latest_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_app_dashboard_configuration_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_composer_cache_on_web_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_spam_report_state_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_unread_spam_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/quick_search_email_interactor.dart'; @@ -88,10 +76,22 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; -import 'package:tmail_ui_user/features/quotas/presentation/quotas_controller_bindings.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/quotas/presentation/quotas_bindings.dart'; import 'package:tmail_ui_user/features/search/email/domain/usecases/refresh_changes_search_email_interactor.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/get_all_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/store_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/update_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/bindings/sending_queue_bindings.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/bindings/sending_queue_interactor_bindings.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/sending_queue_controller.dart'; +import 'package:tmail_ui_user/features/session/domain/repository/session_repository.dart'; +import 'package:tmail_ui_user/features/session/domain/usecases/store_session_interactor.dart'; import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart'; import 'package:tmail_ui_user/features/thread/data/datasource_impl/local_thread_datasource_impl.dart'; import 'package:tmail_ui_user/features/thread/data/datasource_impl/thread_datasource_impl.dart'; @@ -100,6 +100,7 @@ import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_isolate_worker.dart'; import 'package:tmail_ui_user/features/thread/data/repository/thread_repository_impl.dart'; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart'; @@ -117,11 +118,12 @@ class MailboxDashBoardBindings extends BaseBindings { @override void dependencies() { super.dependencies(); + SendingQueueBindings().dependencies(); MailboxBindings().dependencies(); ThreadBindings().dependencies(); EmailBindings().dependencies(); SearchEmailBindings().dependencies(); - QuotasControllerBindings().dependencies(); + QuotasBindings().dependencies(); } @override @@ -138,11 +140,10 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), - Get.find() - )); + Get.find(), + Get.find())); + Get.put(MailboxDashBoardController( - Get.find(), - Get.find(), Get.find(), Get.find(), Get.find(), @@ -157,6 +158,12 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), )); Get.put(AdvancedFilterController()); } @@ -169,8 +176,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); } @@ -182,7 +187,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find())); Get.lazyPut(() => HtmlDataSourceImpl( Get.find(), - Get.find(), Get.find())); Get.lazyPut(() => SearchDataSourceImpl( Get.find(), @@ -200,16 +204,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.lazyPut(() => MailboxCacheDataSourceImpl( Get.find(), Get.find())); - Get.lazyPut(() => HiveAccountDatasourceImpl( - Get.find(), - Get.find())); - Get.lazyPut(() => AuthenticationOIDCDataSourceImpl( - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - )); Get.lazyPut(() => SessionStorageComposerDatasourceImpl()); Get.lazyPut(() => SpamReportDataSourceImpl( Get.find(), @@ -219,6 +213,18 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), )); + Get.lazyPut(() => SpamReportCacheDataSourceImpl( + Get.find(), + Get.find())); + Get.lazyPut(() => EmailHiveCacheDataSourceImpl( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find())); } @override @@ -242,22 +248,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find()) ); - Get.lazyPut(() => LogoutOidcInteractor( - Get.find(), - Get.find(), - )); - Get.lazyPut(() => DeleteAuthorityOidcInteractor( - Get.find(), - Get.find())); - Get.lazyPut(() => GetStoredTokenOidcInteractor( - Get.find(), - Get.find(), - )); - Get.lazyPut(() => GetAuthenticatedAccountInteractor( - Get.find(), - Get.find(), - Get.find(), - )); Get.lazyPut(() => GetComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => SaveComposerCacheOnWebInteractor(Get.find())); Get.lazyPut(() => RemoveComposerCacheOnWebInteractor(Get.find())); @@ -282,10 +272,16 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find(), Get.find(), Get.find())); + Get.lazyPut(() => EmptySpamFolderInteractor( + Get.find(), + Get.find(), + Get.find() + )); Get.lazyPut(() => GetAppDashboardConfigurationInteractor( Get.find())); Get.lazyPut(() => GetEmailByIdInteractor( - Get.find())); + Get.find(), + Get.find())); Get.lazyPut(() => UpdateAuthenticationAccountInteractor(Get.find())); Get.lazyPut(() => StoreSpamReportInteractor( Get.find())); @@ -295,6 +291,13 @@ class MailboxDashBoardBindings extends BaseBindings { Get.find())); Get.lazyPut(() => GetSpamReportStateInteractor( Get.find())); + Get.lazyPut(() => GetSpamMailboxCachedInteractor(Get.find())); + Get.lazyPut(() => SendEmailInteractor( + Get.find(), + Get.find() + )); + SendingQueueInteractorBindings().dependencies(); + Get.lazyPut(() => StoreSessionInteractor(Get.find())); } @override @@ -304,8 +307,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); } @@ -313,9 +314,12 @@ class MailboxDashBoardBindings extends BaseBindings { @override void bindingsRepositoryImpl() { Get.lazyPut(() => EmailRepositoryImpl( - Get.find(), - Get.find(), - Get.find(), + { + DataSourceType.network: Get.find(), + DataSourceType.hiveCache: Get.find() + }, + Get.find(), + Get.find(), )); Get.lazyPut(() => SearchRepositoryImpl(Get.find())); Get.lazyPut(() => ThreadRepositoryImpl( @@ -332,13 +336,12 @@ class MailboxDashBoardBindings extends BaseBindings { }, Get.find(), )); - Get.lazyPut(() => AccountRepositoryImpl(Get.find())); - Get.lazyPut(() => AuthenticationOIDCRepositoryImpl(Get.find())); Get.lazyPut(() => ComposerCacheRepositoryImpl(Get.find())); Get.lazyPut(() => SpamReportRepositoryImpl( { DataSourceType.network: Get.find(), - DataSourceType.local: Get.find() + DataSourceType.local: Get.find(), + DataSourceType.hiveCache: Get.find() }, )); } @@ -349,5 +352,6 @@ class MailboxDashBoardBindings extends BaseBindings { Get.delete(); Get.delete(); Get.delete(); + Get.delete(); } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart index 7da60bd926..8e37f0596e 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart @@ -2,31 +2,31 @@ import 'package:collection/collection.dart'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_date_range_picker/multiple_view_date_range_picker.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/model.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/composer/domain/model/contact_suggestion_source.dart'; import 'package:tmail_ui_user/features/composer/domain/state/get_autocomplete_state.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_interactor.dart'; import 'package:tmail_ui_user/features/composer/domain/usecases/get_autocomplete_with_device_contact_interactor.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/input_field_focus_manager.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/datetime_extension.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class AdvancedFilterController extends BaseController { @@ -37,28 +37,24 @@ class AdvancedFilterController extends BaseController { final hasAttachment = false.obs; final lastTextForm = ''.obs; final lastTextTo = ''.obs; + final startDate = Rxn(); + final endDate = Rxn(); + TextEditingController subjectFilterInputController = TextEditingController(); TextEditingController hasKeyWordFilterInputController = TextEditingController(); TextEditingController notKeyWordFilterInputController = TextEditingController(); - TextEditingController dateFilterInputController = TextEditingController(); TextEditingController mailBoxFilterInputController = TextEditingController(); ContactSuggestionSource _contactSuggestionSource = ContactSuggestionSource.tMailContact; - final SearchController searchController = Get.find(); + final search.SearchController searchController = Get.find(); final MailboxDashBoardController _mailboxDashBoardController = Get.find(); - final _appToast = Get.find(); - final _imagePaths = Get.find(); SearchEmailFilter get searchEmailFilter => searchController.searchEmailFilter.value; final focusManager = InputFieldFocusManager.initial(); - DateTime? _startDate, _endDate; - - DateTime? get startDate => _startDate; - - DateTime? get endDate => _endDate; + PresentationMailbox? _destinationMailboxSelected; late Worker _dashboardActionWorker; @@ -70,7 +66,7 @@ class AdvancedFilterController extends BaseController { @override void onReady() { - if (!BuildUtils.isWeb) { + if (PlatformInfo.isMobile) { Future.delayed( const Duration(milliseconds: 500), () => _checkContactPermission()); @@ -83,17 +79,16 @@ class AdvancedFilterController extends BaseController { void cleanSearchFilter(BuildContext context) { searchController.clearSearchFilter(); - dateFilterSelectedFormAdvancedSearch.value = EmailReceiveTimeType.allTime; - clearDateRangeOfFilter(); + _updateDateRangeTime(EmailReceiveTimeType.allTime); subjectFilterInputController.text = ''; hasKeyWordFilterInputController.text = ''; notKeyWordFilterInputController.text = ''; - dateFilterInputController.text = ''; hasAttachment.value = false; + _destinationMailboxSelected = null; + searchController.searchInputController.clear(); searchController.deactivateAdvancedSearch(); searchController.isAdvancedSearchViewOpen.toggle(); - _mailboxDashBoardController.searchEmail( - context, StringConvert.writeNullToEmpty(searchEmailFilter.text?.value)); + _mailboxDashBoardController.searchEmail(context); } void _updateFilterEmailFromAdvancedSearchView() { @@ -101,7 +96,7 @@ class AdvancedFilterController extends BaseController { searchController.updateFilterEmail(text: SearchQuery(hasKeyWordFilterInputController.text)); searchController.searchInputController.text = hasKeyWordFilterInputController.text; } else { - searchController.updateFilterEmail(text: SearchQuery(searchController.searchInputController.text)); + searchController.updateFilterEmail(text: SearchQuery.initial()); } if (notKeyWordFilterInputController.text.isNotEmpty) { @@ -110,15 +105,12 @@ class AdvancedFilterController extends BaseController { searchController.updateFilterEmail(notKeyword: {}); } - if(lastTextForm.isNotEmpty && !searchController.searchEmailFilter.value.from.contains(lastTextForm.value)){ - searchController.updateFilterEmail( - from: searchController.searchEmailFilter.value.from..add(lastTextForm.value), - ); - + if (lastTextForm.isNotEmpty && !searchController.searchEmailFilter.value.from.contains(lastTextForm.value)){ + searchController.updateFilterEmail(fromOption: Some(searchController.searchEmailFilter.value.from..add(lastTextForm.value))); lastTextForm.value = ''; } - if(lastTextTo.isNotEmpty && !searchController.searchEmailFilter.value.to.contains(lastTextTo.value)){ + if (lastTextTo.isNotEmpty && !searchController.searchEmailFilter.value.to.contains(lastTextTo.value)){ searchController.updateFilterEmail( to: searchController.searchEmailFilter.value.to..add(lastTextTo.value), ); @@ -127,43 +119,39 @@ class AdvancedFilterController extends BaseController { } searchController.updateFilterEmail( + mailbox: _destinationMailboxSelected, subjectOption: optionOf(subjectFilterInputController.text), emailReceiveTimeType: dateFilterSelectedFormAdvancedSearch.value, hasAttachment: hasAttachment.value, - endDate: _endDate.toUTCDate(), - startDate: _startDate.toUTCDate() + startDateOption: optionOf(startDate.value?.toUTCDate()), + endDateOption: optionOf(endDate.value?.toUTCDate()) ); } void selectedMailBox(BuildContext context) async { final accountId = _mailboxDashBoardController.accountId.value; - final _session = _mailboxDashBoardController.sessionCurrent; - if (accountId != null) { + final session = _mailboxDashBoardController.sessionCurrent; + if (accountId != null && session != null) { final arguments = DestinationPickerArguments( - accountId, - MailboxActions.select, - _session, - mailboxIdSelected: searchController.searchEmailFilter.value.mailbox?.id); - - if (BuildUtils.isWeb) { - showDialogDestinationPicker( - context: context, - arguments: arguments, - onSelectedMailbox: (destinationMailbox) { - searchController.updateFilterEmail(mailbox: destinationMailbox); - mailBoxFilterInputController.text = - StringConvert.writeNullToEmpty(destinationMailbox.name?.name); - }); - } else { - final destinationMailbox = await push( - AppRoutes.destinationPicker, - arguments: arguments); - - if (destinationMailbox is PresentationMailbox) { - searchController.updateFilterEmail(mailbox: destinationMailbox); - mailBoxFilterInputController.text = - StringConvert.writeNullToEmpty(destinationMailbox.name?.name); + accountId, + MailboxActions.select, + session, + mailboxIdSelected: _destinationMailboxSelected?.id + ); + + final destinationMailbox = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments) + : await push(AppRoutes.destinationPicker, arguments: arguments); + + if (destinationMailbox is PresentationMailbox) { + _destinationMailboxSelected = destinationMailbox; + String? mailboxName; + if (context.mounted) { + mailboxName = _destinationMailboxSelected?.getDisplayName(context); + } else { + mailboxName = _destinationMailboxSelected?.name?.name; } + mailBoxFilterInputController.text = StringConvert.writeNullToEmpty(mailboxName); } } } @@ -176,12 +164,10 @@ class AdvancedFilterController extends BaseController { searchController.deactivateAdvancedSearch(); } if (!isAdvancedSearchHasApplied) { - final newSearchEmailFilter = searchController.searchEmailFilter.value.clearBeforeDate(); - searchController.searchEmailFilter.value = newSearchEmailFilter; + searchController.updateFilterEmail(beforeOption: const None()); } searchController.isAdvancedSearchViewOpen.toggle(); - _mailboxDashBoardController.searchEmail( - context, StringConvert.writeNullToEmpty(searchEmailFilter.text?.value)); + _mailboxDashBoardController.searchEmail(context); } void _checkContactPermission() async { @@ -259,111 +245,55 @@ class AdvancedFilterController extends BaseController { } void initSearchFilterField(BuildContext context) { - searchController.updateFilterEmail( - mailbox: PresentationMailbox.unifiedMailbox); - subjectFilterInputController.text = - StringConvert.writeNullToEmpty(searchEmailFilter.subject); - hasKeyWordFilterInputController.text = StringConvert.writeNullToEmpty( - searchEmailFilter.text?.value); - notKeyWordFilterInputController.text = StringConvert.writeNullToEmpty( - searchEmailFilter.notKeyword.firstOrNull); - dateFilterInputController.text = StringConvert.writeNullToEmpty( - searchEmailFilter.emailReceiveTimeType.getTitle( - context, - startDate: _startDate, - endDate: _endDate)); - mailBoxFilterInputController.text = - StringConvert.writeNullToEmpty(searchEmailFilter.mailbox?.name?.name); - dateFilterSelectedFormAdvancedSearch.value = - searchEmailFilter.emailReceiveTimeType; + subjectFilterInputController.text = StringConvert.writeNullToEmpty(searchEmailFilter.subject); + hasKeyWordFilterInputController.text = StringConvert.writeNullToEmpty(searchEmailFilter.text?.value); + notKeyWordFilterInputController.text = StringConvert.writeNullToEmpty(searchEmailFilter.notKeyword.firstOrNull); + dateFilterSelectedFormAdvancedSearch.value = searchEmailFilter.emailReceiveTimeType; + _destinationMailboxSelected = searchEmailFilter.mailbox; + if (searchEmailFilter.mailbox == null) { + mailBoxFilterInputController.text = AppLocalizations.of(context).allMailboxes; + } else { + mailBoxFilterInputController.text = StringConvert.writeNullToEmpty(searchEmailFilter.mailbox?.getDisplayName(context)); + } hasAttachment.value = searchEmailFilter.hasAttachment; } void selectDateRange(BuildContext context) { - showGeneralDialog( - context: context, - barrierDismissible: true, - barrierLabel: '', - barrierColor: Colors.black54, - pageBuilder: (context, animation, secondaryAnimation) { - return Dialog( - elevation: 0, - backgroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18.0)), - child: PointerInterceptor( - child: MultipleViewDateRangePicker( - confirmText: AppLocalizations.of(context).setDate, - cancelText: AppLocalizations.of(context).cancel, - last7daysTitle: AppLocalizations.of(context).last7Days, - last30daysTitle: AppLocalizations.of(context).last30Days, - last6monthsTitle: AppLocalizations.of(context).last6Months, - lastYearTitle: AppLocalizations.of(context).lastYears, - startDate: _startDate, - endDate: _endDate, - setDateActionCallback: ({startDate, endDate}) { - _handleSelectDateRangeResult(context, startDate, endDate); - }, - ), - ) - ); - } + searchController.showMultipleViewDateRangePicker( + context, + startDate.value, + endDate.value, + onCallbackAction: (startDate, endDate) => + _updateDateRangeTime( + EmailReceiveTimeType.customRange, + newStartDate: startDate, + newEndDate: endDate + ) ); } - void _handleSelectDateRangeResult( - BuildContext context, - DateTime? startDate, - DateTime? endDate - ) { - log('AdvancedFilterController::_handleSelectDateRangeResult(): startDate: $startDate'); - log('AdvancedFilterController::_handleSelectDateRangeResult(): endDate: $endDate'); - if (startDate == null) { - _appToast.showToastWithIcon( - context, - textColor: Colors.black, - message: AppLocalizations.of(context).toastMessageErrorWhenSelectStartDateIsEmpty, - icon: _imagePaths.icNotConnection); - return; - } - if (endDate == null) { - _appToast.showToastWithIcon( - context, - textColor: Colors.black, - message: AppLocalizations.of(context).toastMessageErrorWhenSelectEndDateIsEmpty, - icon: _imagePaths.icNotConnection); - return; - } - - if (endDate.isBefore(startDate)) { - _appToast.showToastWithIcon( - context, - textColor: Colors.black, - message: AppLocalizations.of(context).toastMessageErrorWhenSelectDateIsInValid, - icon: _imagePaths.icNotConnection); - return; - } - - _startDate = startDate; - _endDate = endDate; - dateFilterSelectedFormAdvancedSearch.value = EmailReceiveTimeType.customRange; - dateFilterInputController.text = EmailReceiveTimeType.customRange.getTitle( - context, - startDate: startDate, - endDate: endDate); - dateFilterSelectedFormAdvancedSearch.refresh(); - - popBack(); + void _updateDateRangeTime(EmailReceiveTimeType receiveTime, {DateTime? newStartDate, DateTime? newEndDate}) { + startDate.value = newStartDate; + endDate.value = newEndDate; + dateFilterSelectedFormAdvancedSearch.value = receiveTime; } - void clearDateRangeOfFilter() { - _startDate = null; - _endDate = null; - - searchController.searchEmailFilter.value = - searchController.searchEmailFilter.value.withDateRange( - startDate: _startDate.toUTCDate(), - endDate: _endDate.toUTCDate()); + void updateReceiveDateSearchFilter(BuildContext context, EmailReceiveTimeType receiveTime) { + if (receiveTime == EmailReceiveTimeType.customRange) { + searchController.showMultipleViewDateRangePicker( + context, + startDate.value, + endDate.value, + onCallbackAction: (startDate, endDate) => + _updateDateRangeTime( + EmailReceiveTimeType.customRange, + newStartDate: startDate, + newEndDate: endDate + ) + ); + } else { + _updateDateRangeTime(receiveTime); + } } void _resetAllToOriginalValue() { @@ -371,15 +301,14 @@ class AdvancedFilterController extends BaseController { hasAttachment.value = false; lastTextForm.value = ''; lastTextTo.value = ''; - _startDate = null; - _endDate = null; + startDate.value = null; + endDate.value = null; } void _clearAllTextFieldInput() { subjectFilterInputController.clear(); hasKeyWordFilterInputController.clear(); notKeyWordFilterInputController.clear(); - dateFilterInputController.clear(); mailBoxFilterInputController.clear(); } @@ -389,6 +318,14 @@ class AdvancedFilterController extends BaseController { (action) { if (action is ClearAllFieldOfAdvancedSearchAction) { _handleClearAllFieldOfAdvancedSearch(); + } else if (action is SelectDateRangeToAdvancedSearch) { + _updateDateRangeTime( + EmailReceiveTimeType.customRange, + newStartDate: action.startDate, + newEndDate: action.endDate + ); + } else if (action is ClearDateRangeToAdvancedSearch) { + _updateDateRangeTime(action.receiveTime); } } ); @@ -410,11 +347,7 @@ class AdvancedFilterController extends BaseController { hasKeyWordFilterInputController.dispose(); notKeyWordFilterInputController.dispose(); mailBoxFilterInputController.dispose(); - dateFilterInputController.dispose(); _unregisterWorkerListener(); super.onClose(); } - - @override - void onDone() {} } diff --git a/lib/features/mailbox_dashboard/presentation/controller/custom_tf_tag_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/custom_tf_tag_controller.dart index 455a2db9b2..40b69d4f84 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/custom_tf_tag_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/custom_tf_tag_controller.dart @@ -33,7 +33,7 @@ class CustomController extends TextfieldTagsController { @override void onChanged(String value) { - final ts = [' ', ',']; + final ts = [',']; final separator = ts.cast().firstWhere( (element) => value.contains(element!) && value.indexOf(element) != 0, orElse: () => null); diff --git a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart index 53bb401a83..d72aab821f 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -24,9 +23,16 @@ import 'package:rxdart/transformers.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; import 'package:tmail_ui_user/features/composer/domain/exceptions/set_email_method_exception.dart'; +import 'package:tmail_ui_user/features/composer/domain/extensions/email_request_extension.dart'; +import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/compose_action_mode.dart'; +import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; +import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; +import 'package:tmail_ui_user/features/email/domain/state/store_sending_email_state.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_bindings.dart'; import 'package:tmail_ui_user/features/composer/presentation/extensions/email_action_type_extension.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; @@ -42,12 +48,14 @@ import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_star_email_ import 'package:tmail_ui_user/features/email/domain/usecases/move_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/mark_as_mailbox_read_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_app_dashboard_configuration_state.dart'; @@ -59,24 +67,31 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/das import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/app_grid_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/download/download_controller.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/extensions/set_error_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/composer_overlay_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/download/download_task_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/draggable_app_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/refresh_action_view_event.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; +import 'package:tmail_ui_user/features/mailto/presentation/model/mailto_arguments.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/update_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_vacation_interactor.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/update_vacation_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/extensions/datetime_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/account_menu_item.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/manage_account_arguments.dart'; -import 'package:tmail_ui_user/features/network_status_handle/presentation/network_connnection_controller.dart'; +import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' + if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; +import 'package:tmail_ui_user/features/offline_mode/config/work_manager_constants.dart'; +import 'package:tmail_ui_user/features/offline_mode/controller/work_manager_controller.dart'; +import 'package:tmail_ui_user/features/offline_mode/work_manager/one_time_work_request.dart'; +import 'package:tmail_ui_user/features/offline_mode/work_manager/worker_type.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/get_email_state_to_refresh_state.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/get_mailbox_state_to_refresh_state.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/delete_email_state_to_refresh_interactor.dart'; @@ -85,28 +100,41 @@ import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_ema import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_mailbox_state_to_refresh_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_manager.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_service.dart'; -import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/state/get_all_sending_email_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/state/update_sending_email_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/get_all_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/store_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/update_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_arguments.dart'; +import 'package:tmail_ui_user/features/session/domain/usecases/store_session_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/usecases/empty_spam_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/empty_trash_folder_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/get_email_by_id_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart'; import 'package:tmail_ui_user/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/navigation_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; -import 'package:tmail_ui_user/main/routes/router_arguments.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:uuid/uuid.dart'; +import 'package:workmanager/workmanager.dart' as work_manager; class MailboxDashBoardController extends ReloadableController { @@ -115,11 +143,12 @@ class MailboxDashBoardController extends ReloadableController { final RemoveEmailDraftsInteractor _removeEmailDraftsInteractor = Get.find(); final ResponsiveUtils _responsiveUtils = Get.find(); final EmailReceiveManager _emailReceiveManager = Get.find(); - final SearchController searchController = Get.find(); + final search.SearchController searchController = Get.find(); final DownloadController downloadController = Get.find(); - final NetworkConnectionController networkConnectionController = Get.find(); final AppGridDashboardController appGridDashboardController = Get.find(); final SpamReportController spamReportController = Get.find(); + final NetworkConnectionController networkConnectionController = Get.find(); + final Uuid _uuid = Get.find(); final MoveToMailboxInteractor _moveToMailboxInteractor; final DeleteEmailPermanentlyInteractor _deleteEmailPermanentlyInteractor; @@ -133,6 +162,12 @@ class MailboxDashBoardController extends ReloadableController { final EmptyTrashFolderInteractor _emptyTrashFolderInteractor; final DeleteMultipleEmailsPermanentlyInteractor _deleteMultipleEmailsPermanentlyInteractor; final GetEmailByIdInteractor _getEmailByIdInteractor; + final SendEmailInteractor _sendEmailInteractor; + final StoreSendingEmailInteractor _storeSendingEmailInteractor; + final UpdateSendingEmailInteractor _updateSendingEmailInteractor; + final GetAllSendingEmailInteractor _getAllSendingEmailInteractor; + final StoreSessionInteractor _storeSessionInteractor; + final EmptySpamFolderInteractor _emptySpamFolderInteractor; GetAllVacationInteractor? _getAllVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor; @@ -160,16 +195,21 @@ class MailboxDashBoardController extends ReloadableController { final routerParameters = Rxn>(); final _isDraggingMailbox = RxBool(false); final searchMailboxActivated = RxBool(false); + final listSendingEmails = RxList(); + final refreshingMailboxState = Rx>(Right(UIState.idle)); + final draggableAppState = Rxn(); + Session? sessionCurrent; Map mapDefaultMailboxIdByRole = {}; Map mapMailboxById = {}; final emailsInCurrentMailbox = [].obs; final listResultSearch = RxList(); PresentationMailbox? outboxMailbox; - RouterArguments? routerArguments; + ComposerArguments? composerArguments; NavigationRouter? navigationRouter; - late StreamSubscription _emailReceiveManagerStreamSubscription; + late StreamSubscription _emailAddressStreamSubscription; + late StreamSubscription _emailContentStreamSubscription; late StreamSubscription _fileReceiveManagerStreamSubscription; final StreamController> _progressStateController = @@ -183,8 +223,6 @@ class MailboxDashBoardController extends ReloadableController { final _fcmService = FcmService.instance; MailboxDashBoardController( - LogoutOidcInteractor logoutOidcInteractor, - DeleteAuthorityOidcInteractor deleteAuthorityOidcInteractor, GetAuthenticatedAccountInteractor getAuthenticatedAccountInteractor, UpdateAuthenticationAccountInteractor updateAuthenticationAccountInteractor, this._moveToMailboxInteractor, @@ -199,9 +237,13 @@ class MailboxDashBoardController extends ReloadableController { this._emptyTrashFolderInteractor, this._deleteMultipleEmailsPermanentlyInteractor, this._getEmailByIdInteractor, + this._sendEmailInteractor, + this._storeSendingEmailInteractor, + this._updateSendingEmailInteractor, + this._getAllSendingEmailInteractor, + this._storeSessionInteractor, + this._emptySpamFolderInteractor, ) : super( - logoutOidcInteractor, - deleteAuthorityOidcInteractor, getAuthenticatedAccountInteractor, updateAuthenticationAccountInteractor ); @@ -215,160 +257,166 @@ class MailboxDashBoardController extends ReloadableController { @override void onReady() { _registerPendingEmailAddress(); + _registerPendingEmailContents(); _registerPendingFileInfo(); - _getSessionCurrent(); + _handleArguments(); _getAppVersion(); super.onReady(); } void _handleComposerCache() async { - if (kIsWeb && userProfile.value != null) { - _getEmailCacheOnWebInteractor.execute().fold( - (failure) {}, - (success) { - if(success is GetComposerCacheSuccess){ - final ComposerArguments composerArguments = ComposerArguments( - emailActionType: EmailActionType.edit, - presentationEmail: PresentationEmail( - success.composerCache.id, - subject: success.composerCache.subject, - from: success.composerCache.from, - to: success.composerCache.to, - cc: success.composerCache.cc, - bcc: success.composerCache.bcc, - ), - emailContents: success.composerCache.emailContentList, - ); - openComposerOverlay(composerArguments); - } - }, - ); - } + _getEmailCacheOnWebInteractor.execute().fold( + (failure) {}, + (success) { + if(success is GetComposerCacheSuccess){ + openComposerOverlay(ComposerArguments.fromSessionStorageBrowser(success.composerCache)); + } + }, + ); } @override - void onData(Either newState) { - super.onData(newState); - viewState.value.fold( - (failure) { - log('MailboxDashBoardController::onData():failure $failure'); - }, - (success) { - log('MailboxDashBoardController::onData():success $success'); - if (success is SendingEmailState) { - if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - message: AppLocalizations.of(currentContext!).your_email_being_sent, - icon: _imagePaths.icSendToast); - } - } else if (success is GetEmailStateToRefreshSuccess) { - dispatchEmailUIAction(RefreshChangeEmailAction(success.storedState)); - _deleteEmailStateToRefreshAction(); - } else if (success is GetMailboxStateToRefreshSuccess) { - dispatchMailboxUIAction(RefreshChangeMailboxAction(success.storedState)); - _deleteMailboxStateToRefreshAction(); - } + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is SendEmailLoading) { + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).your_email_being_sent, + leadingSVGIcon: _imagePaths.icSendToast); } - ); + } else if (success is GetEmailStateToRefreshSuccess) { + dispatchEmailUIAction(RefreshChangeEmailAction(success.storedState)); + _deleteEmailStateToRefreshAction(); + } else if (success is GetMailboxStateToRefreshSuccess) { + dispatchMailboxUIAction(RefreshChangeMailboxAction(success.storedState)); + _deleteMailboxStateToRefreshAction(); + } else if (success is SendEmailSuccess) { + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).message_has_been_sent_successfully, + leadingSVGIcon: _imagePaths.icSendSuccessToast); + } + } else if (success is SaveEmailAsDraftsSuccess) { + _saveEmailAsDraftsSuccess(success); + } else if (success is MoveToMailboxSuccess) { + _moveToMailboxSuccess(success); + } else if (success is DeleteEmailPermanentlySuccess) { + _deleteEmailPermanentlySuccess(success); + } else if (success is MarkAsMailboxReadAllSuccess || + success is MarkAsMailboxReadHasSomeEmailFailure) { + _markAsReadMailboxSuccess(success); + } else if (success is GetAllVacationSuccess) { + if (success.listVacationResponse.isNotEmpty) { + vacationResponse.value = success.listVacationResponse.first; + } + } else if (success is UpdateVacationSuccess) { + _handleUpdateVacationSuccess(success); + } else if (success is MarkAsMultipleEmailReadAllSuccess || + success is MarkAsMultipleEmailReadHasSomeEmailFailure) { + _markAsReadSelectedMultipleEmailSuccess(success); + } else if (success is MarkAsStarMultipleEmailAllSuccess || + success is MarkAsStarMultipleEmailHasSomeEmailFailure) { + _markAsStarMultipleEmailSuccess(success); + } else if (success is MoveMultipleEmailToMailboxAllSuccess || + success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { + _moveSelectedMultipleEmailToMailboxSuccess(success); + } else if (success is EmptyTrashFolderSuccess) { + _emptyTrashFolderSuccess(success); + } else if (success is DeleteMultipleEmailsPermanentlyAllSuccess || + success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { + _deleteMultipleEmailsPermanentlySuccess(success); + } else if (success is GetAppDashboardConfigurationSuccess) { + appGridDashboardController.handleShowAppDashboard(success.linagoraApplications); + } else if(success is GetEmailByIdSuccess) { + _moveToEmailDetailedView(success); + } else if (success is StoreSendingEmailSuccess) { + _handleStoreSendingEmailSuccess(success); + } else if (success is GetAllSendingEmailSuccess) { + _handleGetAllSendingEmailsSuccess(success); + } else if (success is UpdateSendingEmailSuccess) { + _handleUpdateSendingEmailSuccess(success); + } else if (success is EmptySpamFolderSuccess) { + _emptySpamFolderSuccess(success); + } else if (success is MarkAsEmailReadSuccess) { + _markAsReadEmailSuccess(success); + } } @override - void onDone() { - viewState.value.fold( - (failure) { - if (failure is SendEmailFailure) { - _handleSendEmailFailure(failure); - } else if (failure is SaveEmailAsDraftsFailure) { - _handleSaveEmailAsDraftsFailure(failure); - } else if (failure is UpdateEmailDraftsFailure) { - _handleUpdateEmailAsDraftsFailure(failure); - } else if (failure is RemoveEmailDraftsFailure) { - clearState(); - } else if (failure is MarkAsMailboxReadAllFailure || - failure is MarkAsMailboxReadFailure) { - _markAsReadMailboxFailure(failure); - } else if (failure is GetEmailByIdFailure) { - _handleGetEmailDetailedFailed(failure); - } - }, - (success) { - if (success is SendEmailSuccess) { - if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - textColor: AppColor.primaryColor, - message: AppLocalizations.of(currentContext!).message_has_been_sent_successfully, - icon: _imagePaths.icSendToast); - } - } else if (success is SaveEmailAsDraftsSuccess) { - log('MailboxDashBoardController::onDone(): SaveEmailAsDraftsSuccess'); - _saveEmailAsDraftsSuccess(success); - } else if (success is MoveToMailboxSuccess) { - _moveToMailboxSuccess(success); - } else if (success is DeleteEmailPermanentlySuccess) { - _deleteEmailPermanentlySuccess(success); - } else if (success is MarkAsMailboxReadAllSuccess || - success is MarkAsMailboxReadHasSomeEmailFailure) { - _markAsReadMailboxSuccess(success); - } else if (success is GetAllVacationSuccess) { - if (success.listVacationResponse.isNotEmpty) { - vacationResponse.value = success.listVacationResponse.first; - } - } else if (success is UpdateVacationSuccess) { - _handleUpdateVacationSuccess(success); - } else if (success is MarkAsMultipleEmailReadAllSuccess - || success is MarkAsMultipleEmailReadHasSomeEmailFailure) { - _markAsReadSelectedMultipleEmailSuccess(success); - } else if (success is MarkAsStarMultipleEmailAllSuccess - || success is MarkAsStarMultipleEmailHasSomeEmailFailure) { - _markAsStarMultipleEmailSuccess(success); - } else if (success is MoveMultipleEmailToMailboxAllSuccess - || success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { - _moveSelectedMultipleEmailToMailboxSuccess(success); - } else if (success is EmptyTrashFolderSuccess) { - _emptyTrashFolderSuccess(success); - } else if (success is DeleteMultipleEmailsPermanentlyAllSuccess - || success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { - _deleteMultipleEmailsPermanentlySuccess(success); - } else if (success is GetAppDashboardConfigurationSuccess) { - appGridDashboardController.handleShowAppDashboard(success.linagoraApplications); - } else if(success is GetEmailByIdSuccess) { - _moveToEmailDetailedView(success); + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is SendEmailFailure) { + _handleSendEmailFailure(failure); + } else if (failure is SaveEmailAsDraftsFailure) { + _handleSaveEmailAsDraftsFailure(failure); + } else if (failure is UpdateEmailDraftsFailure) { + _handleUpdateEmailAsDraftsFailure(failure); + } else if (failure is RemoveEmailDraftsFailure) { + clearState(); + } else if (failure is MarkAsMailboxReadAllFailure || + failure is MarkAsMailboxReadFailure) { + _markAsReadMailboxFailure(failure); + } else if (failure is GetEmailByIdFailure) { + _handleGetEmailDetailedFailed(failure); + } + } + + @override + void handleExceptionAction({Failure? failure, Exception? exception}) { + super.handleExceptionAction(failure: failure, exception: exception); + if (failure is SendEmailFailure && exception is NoNetworkError) { + if (PlatformInfo.isIOS && currentContext != null) { + _showToastSendMessageFailure(AppLocalizations.of(currentContext!).sendMessageFailure); + } else if (PlatformInfo.isAndroid) { + if (failure.emailRequest.storedSendingId != null) { + _handleStoreSendingEmail( + failure.session, + failure.accountId, + failure.emailRequest, + failure.mailboxRequest + ); + } else { + _handleUpdateSendingEmail( + failure.session, + failure.accountId, + failure.emailRequest, + failure.mailboxRequest + ); } } - ); + } } void _registerPendingEmailAddress() { - _emailReceiveManagerStreamSubscription = - _emailReceiveManager.pendingEmailAddressInfo.stream.listen((emailAddress) { - log('MailboxDashBoardController::_registerPendingEmailAddress(): ${emailAddress?.email}'); - if (emailAddress != null && emailAddress.email?.isNotEmpty == true) { - _emailReceiveManager.clearPendingEmailAddress(); - final arguments = ComposerArguments( - emailActionType: EmailActionType.composeFromEmailAddress, - emailAddress: emailAddress, - mailboxRole: selectedMailbox.value?.role); - goToComposer(arguments); - } - }); + _emailAddressStreamSubscription = + _emailReceiveManager.pendingEmailAddressInfo.stream.listen((emailAddress) { + if (emailAddress?.email?.isNotEmpty == true) { + _emailReceiveManager.clearPendingEmailAddress(); + goToComposer(ComposerArguments.fromEmailAddress(emailAddress!)); + } + }); + } + + void _registerPendingEmailContents() { + _emailContentStreamSubscription = + _emailReceiveManager.pendingEmailContentInfo.stream.listen((emailContent) { + if (emailContent?.content.isNotEmpty == true) { + _emailReceiveManager.clearPendingEmailContent(); + goToComposer(ComposerArguments.fromContentShared([emailContent!].asHtmlString)); + } + }); } void _registerPendingFileInfo() { _fileReceiveManagerStreamSubscription = - _emailReceiveManager.pendingFileInfo.stream.listen((listFile) { - log('MailboxDashBoardController::_registerPendingFileInfo(): ${listFile.length}'); - if (listFile.isNotEmpty && sessionCurrent != null) { - _emailReceiveManager.clearPendingFileInfo(); - final arguments = ComposerArguments( - emailActionType: EmailActionType.edit, - mailboxRole: selectedMailbox.value?.role, - listSharedMediaFile: listFile, - ); - goToComposer(arguments); - } - }); + _emailReceiveManager.pendingFileInfo.stream.listen((listFile) { + if (listFile.isNotEmpty) { + _emailReceiveManager.clearPendingFileInfo(); + goToComposer(ComposerArguments.fromFileShared(listFile)); + } + }); } void _registerStreamListener() { @@ -394,30 +442,13 @@ class MailboxDashBoardController extends ReloadableController { _handleMessageFromNotification(notificationResponse?.payload, onForeground: false); } - void _getUserProfile() async { - userProfile.value = sessionCurrent != null ? UserProfile(sessionCurrent!.username.value) : null; - } - - void _getSessionCurrent() { + void _handleArguments() { final arguments = Get.arguments; log('MailboxDashBoardController::_getSessionCurrent(): arguments = $arguments'); if (arguments is Session) { - sessionCurrent = arguments; - accountId.value = sessionCurrent?.accounts.keys.first; - _getUserProfile(); - updateAuthenticationAccount(sessionCurrent, accountId.value); - injectAutoCompleteBindings(sessionCurrent, accountId.value); - injectRuleFilterBindings(sessionCurrent, accountId.value); - injectVacationBindings(sessionCurrent, accountId.value); - injectFCMBindings(sessionCurrent, accountId.value); - _getVacationResponse(); - - if (!BuildUtils.isWeb && !_notificationManager.isNotificationClickedOnTerminate) { - _handleClickLocalNotificationOnTerminated(); - } else { - dispatchRoute(DashboardRoutes.thread); - } - showSpamReportBanner(); + _handleSession(arguments); + } else if (arguments is MailtoArguments) { + _handleMailtoURL(arguments); } else { dispatchRoute(DashboardRoutes.thread); reload(); @@ -446,6 +477,48 @@ class MailboxDashBoardController extends ReloadableController { } } + void _handleSession(Session session) { + log('MailboxDashBoardController::_handleSession:'); + _setUpComponentsFromSession(session); + + updateAuthenticationAccount( + sessionCurrent!, + accountId.value!, + sessionCurrent!.username + ); + + if (PlatformInfo.isMobile && !_notificationManager.isNotificationClickedOnTerminate) { + _handleClickLocalNotificationOnTerminated(); + } else { + dispatchRoute(DashboardRoutes.thread); + } + } + + void _setUpComponentsFromSession(Session session) { + sessionCurrent = session; + accountId.value = sessionCurrent!.personalAccount.accountId; + userProfile.value = UserProfile(sessionCurrent!.username.value); + + injectAutoCompleteBindings(sessionCurrent, accountId.value); + injectRuleFilterBindings(sessionCurrent, accountId.value); + injectVacationBindings(sessionCurrent, accountId.value); + injectFCMBindings(sessionCurrent, accountId.value); + + _getVacationResponse(); + spamReportController.getSpamReportStateAction(); + + if (PlatformInfo.isMobile) { + getAllSendingEmails(); + _storeSessionAction(sessionCurrent!); + } + } + + void _handleMailtoURL(MailtoArguments arguments) { + log('MailboxDashBoardController::_handleMailtoURL:'); + routerParameters.value = arguments.toMapRouter(); + _handleSession(arguments.session); + } + Future _getAppVersion() async { final info = await PackageInfo.fromPlatform(); log('MailboxDashBoardController::_getAppVersion(): ${info.version}'); @@ -497,9 +570,9 @@ class MailboxDashBoardController extends ReloadableController { void openEmailDetailedView(PresentationEmail presentationEmail) { setSelectedEmail(presentationEmail); dispatchRoute(DashboardRoutes.emailDetailed); - if (BuildUtils.isWeb && presentationEmail.routeWeb != null) { + if (PlatformInfo.isWeb && presentationEmail.routeWeb != null) { RouteUtils.updateRouteOnBrowser( - 'Email-${presentationEmail.id.id.value}', + 'Email-${presentationEmail.id?.id.value ?? ''}', presentationEmail.routeWeb! ); } @@ -510,7 +583,9 @@ class MailboxDashBoardController extends ReloadableController { } void closeMailboxMenuDrawer() { - scaffoldKey.currentState?.openEndDrawer(); + if (isDrawerOpen) { + scaffoldKey.currentState?.openEndDrawer(); + } } void hideMailboxMenuWhenScreenSizeChange(BuildContext context) { @@ -525,22 +600,23 @@ class MailboxDashBoardController extends ReloadableController { bool isSelectionEnabled() => currentSelectMode.value == SelectMode.ACTIVE; - void searchEmail(BuildContext context, String value) { + void searchEmail(BuildContext context, {String? queryString}) { + log('MailboxDashBoardController::searchEmail():'); clearFilterMessageOption(); - searchController.updateFilterEmail(text: SearchQuery(value)); + searchController.clearFilterSuggestion(); + if (queryString?.isNotEmpty == true) { + searchController.updateFilterEmail(text: SearchQuery(queryString!)); + } dispatchAction(StartSearchEmailAction()); - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); if (_searchInsideEmailDetailedViewIsActive(context)) { _closeEmailDetailedView(); } - if (value.isEmpty){ - searchController.setEmailReceiveTimeType(null); - } _unSelectedMailbox(); } bool _searchInsideEmailDetailedViewIsActive(BuildContext context) { - return BuildUtils.isWeb + return PlatformInfo.isWeb && _responsiveUtils.isDesktop(context) && dashboardRoute.value == DashboardRoutes.emailDetailed; } @@ -562,105 +638,98 @@ class MailboxDashBoardController extends ReloadableController { void _saveEmailAsDraftsSuccess(SaveEmailAsDraftsSuccess success) { if (currentContext != null && currentOverlayContext != null) { - _appToast.showBottomToast( - currentOverlayContext!, - AppLocalizations.of(currentContext!).drafts_saved, - actionName: AppLocalizations.of(currentContext!).discard, - onActionClick: () => _discardEmail(success.emailAsDrafts), - leadingIcon: SvgPicture.asset( - _imagePaths.icMailboxDrafts, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastSuccessBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - actionIcon: SvgPicture.asset(_imagePaths.icUndo), - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!) - ); + _appToast.showToastMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).drafts_saved, + actionName: AppLocalizations.of(currentContext!).discard, + onActionClick: () => _discardEmail(success.emailAsDrafts), + leadingSVGIcon: _imagePaths.icMailboxDrafts, + leadingSVGIconColor: Colors.white, + backgroundColor: AppColor.toastSuccessBackgroundColor, + textColor: Colors.white, + actionIcon: SvgPicture.asset(_imagePaths.icUndo)); } } - void moveToMailbox(AccountId accountId, MoveToMailboxRequest moveRequest) { - consumeState(_moveToMailboxInteractor.execute(accountId, moveRequest)); + void moveToMailbox(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { + consumeState(_moveToMailboxInteractor.execute(session, accountId, moveRequest)); } void _moveToMailboxSuccess(MoveToMailboxSuccess success) { if (success.moveAction == MoveAction.moving && currentContext != null && currentOverlayContext != null) { - _appToast.showBottomToast( - currentOverlayContext!, - success.emailActionType.getToastMessageMoveToMailboxSuccess(currentContext!, destinationPath: success.destinationPath), - actionName: AppLocalizations.of(currentContext!).undo, - onActionClick: () { - _revertedToOriginalMailbox(MoveToMailboxRequest( - {success.destinationMailboxId: [success.emailId]}, - success.currentMailboxId, - MoveAction.undo, - sessionCurrent!, - success.emailActionType)); - }, - leadingIcon: SvgPicture.asset( - _imagePaths.icFolderMailbox, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastSuccessBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - actionIcon: SvgPicture.asset(_imagePaths.icUndo), - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!) - ); + _appToast.showToastMessage( + currentOverlayContext!, + success.emailActionType.getToastMessageMoveToMailboxSuccess(currentContext!, destinationPath: success.destinationPath), + actionName: AppLocalizations.of(currentContext!).undo, + onActionClick: () { + _revertedToOriginalMailbox(MoveToMailboxRequest( + {success.destinationMailboxId: [success.emailId]}, + success.currentMailboxId, + MoveAction.undo, + sessionCurrent!, + success.emailActionType + )); + }, + leadingSVGIcon: _imagePaths.icFolderMailbox, + leadingSVGIconColor: Colors.white, + backgroundColor: AppColor.toastSuccessBackgroundColor, + textColor: Colors.white, + actionIcon: SvgPicture.asset(_imagePaths.icUndo)); } } void _revertedToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { final currentAccountId = accountId.value; - if (currentAccountId != null) { - consumeState(_moveToMailboxInteractor.execute(currentAccountId, newMoveRequest)); + final session = sessionCurrent; + if (currentAccountId != null && session != null) { + consumeState(_moveToMailboxInteractor.execute(session, currentAccountId, newMoveRequest)); } } void _discardEmail(Email email) { final currentAccountId = accountId.value; - if (currentAccountId != null) { - consumeState(_removeEmailDraftsInteractor.execute(currentAccountId, email.id)); + final session = sessionCurrent; + if (currentAccountId != null && session != null && email.id != null) { + consumeState(_removeEmailDraftsInteractor.execute(session, currentAccountId, email.id!)); } } void deleteEmailPermanently(PresentationEmail email) { final currentAccountId = accountId.value; - if (currentAccountId != null) { - consumeState(_deleteEmailPermanentlyInteractor.execute(currentAccountId, email.id)); + final session = sessionCurrent; + if (currentAccountId != null && session != null && email.id != null) { + consumeState(_deleteEmailPermanentlyInteractor.execute(session, currentAccountId, email.id!)); } } void _deleteEmailPermanentlySuccess(DeleteEmailPermanentlySuccess success) { - if (currentContext != null && currentOverlayContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - widthToast: _responsiveUtils.isDesktop(currentContext!) ? 360 : null, - message: AppLocalizations.of(currentContext!).toast_message_delete_a_email_permanently_success, - icon: _imagePaths.icDeleteToast); + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).toast_message_delete_a_email_permanently_success, + leadingSVGIcon: _imagePaths.icDeleteToast + ); } } - void markAsEmailRead(PresentationEmail presentationEmail, ReadActions readActions) async { - if (accountId.value != null) { + void markAsEmailRead(PresentationEmail presentationEmail, ReadActions readActions, MarkReadAction markReadAction) async { + if (accountId.value != null && sessionCurrent != null) { consumeState(_markAsEmailReadInteractor.execute( - accountId.value!, - presentationEmail.toEmail(), - readActions)); + sessionCurrent!, + accountId.value!, + presentationEmail.toEmail(), + readActions, + markReadAction)); } } void markAsStarEmail(PresentationEmail presentationEmail, MarkStarAction action) { - if (accountId.value != null) { + if (accountId.value != null && sessionCurrent != null) { consumeState(_markAsStarEmailInteractor.execute( - accountId.value!, - presentationEmail.toEmail(), - action)); + sessionCurrent!, + accountId.value!, + presentationEmail.toEmail(), + action)); } } @@ -669,11 +738,12 @@ class MailboxDashBoardController extends ReloadableController { .map((presentationEmail) => presentationEmail.toEmail()) .toList(); log('MailboxDashBoardController::markAsReadSelectedMultipleEmail(): listEmail: ${listEmail.length}'); - if (accountId.value != null) { + if (accountId.value != null && sessionCurrent != null) { consumeState(_markAsMultipleEmailReadInteractor.execute( - accountId.value!, - listEmail, - readActions)); + sessionCurrent!, + accountId.value!, + listEmail, + readActions)); } } @@ -686,16 +756,51 @@ class MailboxDashBoardController extends ReloadableController { readActions = success.readActions; } - if (currentContext != null && readActions != null && currentOverlayContext != null) { + if (readActions != null && currentContext != null && currentOverlayContext != null) { final message = readActions == ReadActions.markAsUnread - ? AppLocalizations.of(currentContext!).marked_message_toast(AppLocalizations.of(currentContext!).unread) - : AppLocalizations.of(currentContext!).marked_message_toast(AppLocalizations.of(currentContext!).read); - _appToast.showToastWithIcon( - currentOverlayContext!, - message: message, - icon: readActions == ReadActions.markAsUnread - ? _imagePaths.icUnreadToast - : _imagePaths.icReadToast); + ? AppLocalizations.of(currentContext!).marked_message_toast(AppLocalizations.of(currentContext!).unread) + : AppLocalizations.of(currentContext!).marked_message_toast(AppLocalizations.of(currentContext!).read); + + _appToast.showToastSuccessMessage( + currentOverlayContext!, + message, + leadingSVGIcon: readActions == ReadActions.markAsUnread + ? _imagePaths.icUnreadToast + : _imagePaths.icReadToast + ); + } + } + + void _markAsReadEmailSuccess(Success success) { + ReadActions? readActions; + MarkReadAction? markReadAction; + PresentationEmail? presentationEmail; + + if (success is MarkAsEmailReadSuccess) { + readActions = success.readActions; + markReadAction = success.markReadAction; + presentationEmail = success.updatedEmail.toPresentationEmail(); + } + + if (readActions != null && currentContext != null && currentOverlayContext != null && markReadAction == MarkReadAction.swipeOnThread) { + final message = readActions == ReadActions.markAsUnread + ? AppLocalizations.of(currentContext!).markedSingleMessageToast(AppLocalizations.of(currentContext!).unread.toLowerCase()) + : AppLocalizations.of(currentContext!).markedSingleMessageToast(AppLocalizations.of(currentContext!).read.toLowerCase()); + + final undoAction = readActions == ReadActions.markAsUnread ? ReadActions.markAsRead : ReadActions.markAsUnread; + + _appToast.showToastMessage( + currentOverlayContext!, + message, + actionName: AppLocalizations.of(currentContext!).undo, + onActionClick: () { + markAsEmailRead(presentationEmail!, undoAction, MarkReadAction.undo); + }, + leadingSVGIcon: _imagePaths.icToastSuccessMessage, + backgroundColor: AppColor.toastSuccessBackgroundColor, + textColor: Colors.white, + actionIcon: SvgPicture.asset(_imagePaths.icUndo), + ); } } @@ -703,11 +808,12 @@ class MailboxDashBoardController extends ReloadableController { final listEmail = listPresentationEmail .map((presentationEmail) => presentationEmail.toEmail()) .toList(); - if (accountId.value != null) { + if (accountId.value != null && sessionCurrent != null) { consumeState(_markAsStarMultipleEmailInteractor.execute( - accountId.value!, - listEmail, - markStarAction)); + sessionCurrent!, + accountId.value!, + listEmail, + markStarAction)); } } @@ -723,16 +829,19 @@ class MailboxDashBoardController extends ReloadableController { countMarkStarSuccess = success.countMarkStarSuccess; } - if (currentContext != null && markStarAction != null && currentOverlayContext != null) { + if (markStarAction != null) { final message = markStarAction == MarkStarAction.unMarkStar - ? AppLocalizations.of(currentContext!).marked_unstar_multiple_item(countMarkStarSuccess) - : AppLocalizations.of(currentContext!).marked_star_multiple_item(countMarkStarSuccess); - _appToast.showToastWithIcon( + ? AppLocalizations.of(currentContext!).marked_unstar_multiple_item(countMarkStarSuccess) + : AppLocalizations.of(currentContext!).marked_star_multiple_item(countMarkStarSuccess); + + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastMessage( currentOverlayContext!, - message: message, - icon: markStarAction == MarkStarAction.unMarkStar - ? _imagePaths.icUnStar - : _imagePaths.icStar); + message, + leadingSVGIcon: markStarAction == MarkStarAction.unMarkStar + ? _imagePaths.icUnStar + : _imagePaths.icStar); + } } } @@ -745,37 +854,25 @@ class MailboxDashBoardController extends ReloadableController { final arguments = DestinationPickerArguments( accountId.value!, MailboxActions.moveEmail, - sessionCurrent); - - if (BuildUtils.isWeb) { - showDialogDestinationPicker( - context: context, - arguments: arguments, - onSelectedMailbox: (destinationMailbox) { - if (sessionCurrent != null) { - _dispatchMoveToMultipleAction( - accountId.value!, - sessionCurrent!, - listEmails.listEmailIds, - currentMailbox, - destinationMailbox); - } - }); - } else { - final destinationMailbox = await push( - AppRoutes.destinationPicker, - arguments: arguments); - - if (destinationMailbox != null && - destinationMailbox is PresentationMailbox && - sessionCurrent != null) { - _dispatchMoveToMultipleAction( - accountId.value!, - sessionCurrent!, - listEmails.listEmailIds, - currentMailbox, - destinationMailbox); - } + sessionCurrent, + mailboxIdSelected: currentMailbox.mailboxId); + + final destinationMailbox = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments) + : await push(AppRoutes.destinationPicker, arguments: arguments); + + if (destinationMailbox != null && + destinationMailbox is PresentationMailbox && + sessionCurrent != null && + accountId.value != null + ) { + _dispatchMoveToMultipleAction( + accountId.value!, + sessionCurrent!, + listEmails.listEmailIds, + currentMailbox, + destinationMailbox + ); } } } @@ -788,21 +885,30 @@ class MailboxDashBoardController extends ReloadableController { PresentationMailbox destinationMailbox ) { if (destinationMailbox.isTrash) { - _moveSelectedEmailMultipleToMailboxAction(accountId, MoveToMailboxRequest( + _moveSelectedEmailMultipleToMailboxAction( + session, + accountId, + MoveToMailboxRequest( {currentMailbox.id: listEmailIds}, destinationMailbox.id, MoveAction.moving, session, EmailActionType.moveToTrash)); } else if (destinationMailbox.isSpam) { - _moveSelectedEmailMultipleToMailboxAction(accountId, MoveToMailboxRequest( + _moveSelectedEmailMultipleToMailboxAction( + session, + accountId, + MoveToMailboxRequest( {currentMailbox.id: listEmailIds}, destinationMailbox.id, MoveAction.moving, session, EmailActionType.moveToSpam)); } else { - _moveSelectedEmailMultipleToMailboxAction(accountId, MoveToMailboxRequest( + _moveSelectedEmailMultipleToMailboxAction( + session, + accountId, + MoveToMailboxRequest( {currentMailbox.id: listEmailIds}, destinationMailbox.id, MoveAction.moving, @@ -816,22 +922,21 @@ class MailboxDashBoardController extends ReloadableController { List listEmails, PresentationMailbox destinationMailbox, ) { - if(searchController.isSearchEmailRunning){ + if (searchController.isSearchEmailRunning){ final Map> mapListEmailSelectedByMailBoxId = {}; for (var element in listEmails) { final mailbox = element.findMailboxContain(mapMailboxById); - if(mailbox != null) { - if(mapListEmailSelectedByMailBoxId.containsKey(mailbox.id)) { - mapListEmailSelectedByMailBoxId[mailbox.id]?.add(element.id); + if (mailbox != null && element.id != null) { + if (mapListEmailSelectedByMailBoxId.containsKey(mailbox.id)) { + mapListEmailSelectedByMailBoxId[mailbox.id]?.add(element.id!); } else { - mapListEmailSelectedByMailBoxId.addAll({mailbox.id: [element.id]}); + mapListEmailSelectedByMailBoxId.addAll({mailbox.id: [element.id!]}); } } } _handleDragSelectedMultipleEmailToMailboxAction(mapListEmailSelectedByMailBoxId, destinationMailbox); - } else { - if(selectedMailbox.value != null) { + if (selectedMailbox.value != null) { _handleDragSelectedMultipleEmailToMailboxAction({selectedMailbox.value!.id: listEmails.listEmailIds}, destinationMailbox); } } @@ -842,9 +947,11 @@ class MailboxDashBoardController extends ReloadableController { Map> mapListEmails, PresentationMailbox destinationMailbox, ) async { - if (accountId.value != null ) { + if (accountId.value != null && sessionCurrent != null) { if (destinationMailbox.isTrash) { - moveToMailbox(accountId.value!, + moveToMailbox( + sessionCurrent!, + accountId.value!, MoveToMailboxRequest( mapListEmails, destinationMailbox.id, @@ -854,7 +961,9 @@ class MailboxDashBoardController extends ReloadableController { ), ); } else if (destinationMailbox.isSpam) { - moveToMailbox(accountId.value!, + moveToMailbox( + sessionCurrent!, + accountId.value!, MoveToMailboxRequest( mapListEmails, destinationMailbox.id, @@ -864,7 +973,9 @@ class MailboxDashBoardController extends ReloadableController { ), ); } else { - moveToMailbox(accountId.value!, + moveToMailbox( + sessionCurrent!, + accountId.value!, MoveToMailboxRequest( mapListEmails, destinationMailbox.id, @@ -880,10 +991,11 @@ class MailboxDashBoardController extends ReloadableController { } void _moveSelectedEmailMultipleToMailboxAction( - AccountId accountId, - MoveToMailboxRequest moveRequest + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest ) { - consumeState(_moveMultipleEmailToMailboxInteractor.execute(accountId, moveRequest)); + consumeState(_moveMultipleEmailToMailboxInteractor.execute(session, accountId, moveRequest)); } void _moveSelectedMultipleEmailToMailboxSuccess(Success success) { @@ -914,52 +1026,51 @@ class MailboxDashBoardController extends ReloadableController { currentOverlayContext != null && emailActionType != null && moveAction == MoveAction.moving) { - _appToast.showBottomToast( - currentOverlayContext!, - emailActionType.getToastMessageMoveToMailboxSuccess( - currentContext!, - destinationPath: destinationPath), - actionName: AppLocalizations.of(currentContext!).undo, - onActionClick: () { - final newCurrentMailboxId = destinationMailboxId; - final newDestinationMailboxId = currentMailboxId; - if (newCurrentMailboxId != null && newDestinationMailboxId != null) { - _revertedSelectionEmailToOriginalMailbox(MoveToMailboxRequest( - {newCurrentMailboxId: movedEmailIds}, - newDestinationMailboxId, - MoveAction.undo, - sessionCurrent!, - emailActionType!, - destinationPath: destinationPath)); - } - }, - leadingIcon: SvgPicture.asset( - _imagePaths.icFolderMailbox, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastSuccessBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - actionIcon: SvgPicture.asset(_imagePaths.icUndo), - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!) + _appToast.showToastMessage( + currentOverlayContext!, + emailActionType.getToastMessageMoveToMailboxSuccess( + currentContext!, + destinationPath: destinationPath), + actionName: AppLocalizations.of(currentContext!).undo, + onActionClick: () { + final newCurrentMailboxId = destinationMailboxId; + final newDestinationMailboxId = currentMailboxId; + if (newCurrentMailboxId != null && newDestinationMailboxId != null) { + _revertedSelectionEmailToOriginalMailbox(MoveToMailboxRequest( + {newCurrentMailboxId: movedEmailIds}, + newDestinationMailboxId, + MoveAction.undo, + sessionCurrent!, + emailActionType!, + destinationPath: destinationPath + )); + } + }, + leadingSVGIconColor: Colors.white, + leadingSVGIcon: _imagePaths.icFolderMailbox, + backgroundColor: AppColor.toastSuccessBackgroundColor, + textColor: Colors.white, + actionIcon: SvgPicture.asset(_imagePaths.icUndo), ); } } void _revertedSelectionEmailToOriginalMailbox(MoveToMailboxRequest newMoveRequest) { - if (accountId.value != null) { + if (accountId.value != null && sessionCurrent != null) { consumeState(_moveMultipleEmailToMailboxInteractor.execute( - accountId.value!, - newMoveRequest)); + sessionCurrent!, + accountId.value!, + newMoveRequest)); } } void moveSelectedMultipleEmailToTrash(List listEmails, PresentationMailbox mailboxCurrent) { final trashMailboxId = getMailboxIdByRole(PresentationMailbox.roleTrash); - if (accountId.value != null && trashMailboxId != null) { - _moveSelectedEmailMultipleToMailboxAction(accountId.value!, MoveToMailboxRequest( + if (accountId.value != null && trashMailboxId != null && sessionCurrent != null) { + _moveSelectedEmailMultipleToMailboxAction( + sessionCurrent!, + accountId.value!, + MoveToMailboxRequest( {mailboxCurrent.id: listEmails.listEmailIds}, trashMailboxId, MoveAction.moving, @@ -971,8 +1082,11 @@ class MailboxDashBoardController extends ReloadableController { void moveSelectedMultipleEmailToSpam(List listEmail, PresentationMailbox mailboxCurrent) { final spamMailboxId = getMailboxIdByRole(PresentationMailbox.roleSpam); - if (accountId.value != null && spamMailboxId != null) { - _moveSelectedEmailMultipleToMailboxAction(accountId.value!, MoveToMailboxRequest( + if (accountId.value != null && spamMailboxId != null && sessionCurrent != null) { + _moveSelectedEmailMultipleToMailboxAction( + sessionCurrent!, + accountId.value!, + MoveToMailboxRequest( {mailboxCurrent.id: listEmail.listEmailIds}, spamMailboxId, MoveAction.moving, @@ -985,8 +1099,11 @@ class MailboxDashBoardController extends ReloadableController { void unSpamSelectedMultipleEmail(List listEmail) { final spamMailboxId = getMailboxIdByRole(PresentationMailbox.roleSpam); final inboxMailboxId = getMailboxIdByRole(PresentationMailbox.roleInbox); - if (inboxMailboxId != null && accountId.value != null && spamMailboxId != null) { - _moveSelectedEmailMultipleToMailboxAction(accountId.value!, MoveToMailboxRequest( + if (inboxMailboxId != null && accountId.value != null && spamMailboxId != null && sessionCurrent != null) { + _moveSelectedEmailMultipleToMailboxAction( + sessionCurrent!, + accountId.value!, + MoveToMailboxRequest( {spamMailboxId: listEmail.listEmailIds}, inboxMailboxId, MoveAction.moving, @@ -1010,7 +1127,7 @@ class MailboxDashBoardController extends ReloadableController { ..messageText(actionType.getContentDialog( context, count: listEmails?.length, - mailboxName: mailboxCurrent?.name?.name)) + mailboxName: mailboxCurrent?.getDisplayName(context))) ..onCancelAction(AppLocalizations.of(context).cancel, () => popBack()) ..onConfirmAction( actionType.getConfirmActionName(context), @@ -1029,7 +1146,7 @@ class MailboxDashBoardController extends ReloadableController { ..content(actionType.getContentDialog( context, count: listEmails?.length, - mailboxName: mailboxCurrent?.name?.name)) + mailboxName: mailboxCurrent?.getDisplayName(context))) ..addIcon(SvgPicture.asset(_imagePaths.icRemoveDialog, fit: BoxFit.fill)) ..colorConfirmButton(AppColor.colorConfirmActionDialog) ..styleTextConfirmButton(const TextStyle( @@ -1057,7 +1174,7 @@ class MailboxDashBoardController extends ReloadableController { switch(actionType) { case DeleteActionType.all: - _emptyTrashFolderAction(onCancelSelectionEmail: onCancelSelectionEmail); + emptyTrashFolderAction(onCancelSelectionEmail: onCancelSelectionEmail); break; case DeleteActionType.multiple: _deleteMultipleEmailsPermanently(listEmails ?? [], onCancelSelectionEmail: onCancelSelectionEmail); @@ -1067,32 +1184,31 @@ class MailboxDashBoardController extends ReloadableController { } } - void _emptyTrashFolderAction({Function? onCancelSelectionEmail}) { + void emptyTrashFolderAction({Function? onCancelSelectionEmail, MailboxId? trashFolderId}) { onCancelSelectionEmail?.call(); - final trashMailboxId = mapDefaultMailboxIdByRole[PresentationMailbox.roleTrash]; - if (accountId.value != null && trashMailboxId != null) { - consumeState(_emptyTrashFolderInteractor.execute(accountId.value!, trashMailboxId)); + final trashMailboxId = trashFolderId ?? mapDefaultMailboxIdByRole[PresentationMailbox.roleTrash]; + if (sessionCurrent != null && accountId.value != null && trashMailboxId != null) { + consumeState(_emptyTrashFolderInteractor.execute(sessionCurrent!, accountId.value!, trashMailboxId)); } } void _emptyTrashFolderSuccess(EmptyTrashFolderSuccess success) { - if (currentContext != null && currentOverlayContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - widthToast: _responsiveUtils.isDesktop(currentContext!) ? 360 : null, - message: AppLocalizations.of(currentContext!).toast_message_empty_trash_folder_success, - icon: _imagePaths.icDeleteToast); + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).toast_message_empty_trash_folder_success); } } void _deleteMultipleEmailsPermanently(List listEmails, {Function? onCancelSelectionEmail}) { onCancelSelectionEmail?.call(); - if (accountId.value != null) { + if (accountId.value != null && sessionCurrent != null) { consumeState(_deleteMultipleEmailsPermanentlyInteractor.execute( - accountId.value!, - listEmails.listEmailIds)); + sessionCurrent!, + accountId.value!, + listEmails.listEmailIds)); } } @@ -1104,12 +1220,10 @@ class MailboxDashBoardController extends ReloadableController { listEmailIdResult = success.emailIds; } - if (currentContext != null && currentOverlayContext != null && listEmailIdResult.isNotEmpty) { - _appToast.showToastWithIcon( - currentOverlayContext!, - widthToast: _responsiveUtils.isDesktop(currentContext!) ? 360 : null, - message: AppLocalizations.of(currentContext!).toast_message_delete_multiple_email_permanently_success(listEmailIdResult.length), - icon: _imagePaths.icDeleteToast); + if (currentOverlayContext != null && currentContext != null && listEmailIdResult.isNotEmpty) { + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).toast_message_delete_multiple_email_permanently_success(listEmailIdResult.length)); } } @@ -1128,16 +1242,20 @@ class MailboxDashBoardController extends ReloadableController { emailUIAction.value = newAction; } - void openComposerOverlay(RouterArguments? arguments) { - routerArguments = arguments; + void openComposerOverlay(ComposerArguments? arguments) { + composerArguments = arguments; ComposerBindings().dependencies(); composerOverlayState.value = ComposerOverlayState.active; } - void closeComposerOverlay() { - routerArguments = null; + void closeComposerOverlay({dynamic result}) { + composerArguments = null; ComposerBindings().dispose(); composerOverlayState.value = ComposerOverlayState.inActive; + + if (result is SendingEmailArguments) { + handleSendEmailAction(result); + } } void dispatchRoute(DashboardRoutes route) { @@ -1153,16 +1271,10 @@ class MailboxDashBoardController extends ReloadableController { void handleReloaded(Session session) { log('MailboxDashBoardController::handleReloaded():'); _getRouteParameters(); - sessionCurrent = session; - accountId.value = sessionCurrent?.accounts.keys.first; - _getUserProfile(); - _handleComposerCache(); - injectAutoCompleteBindings(sessionCurrent, accountId.value); - injectRuleFilterBindings(sessionCurrent, accountId.value); - injectFCMBindings(sessionCurrent, accountId.value); - injectVacationBindings(sessionCurrent, accountId.value); - _getVacationResponse(); - showSpamReportBanner(); + _setUpComponentsFromSession(session); + if (PlatformInfo.isWeb) { + _handleComposerCache(); + } } void _getRouteParameters() { @@ -1190,28 +1302,36 @@ class MailboxDashBoardController extends ReloadableController { filterMessageOption.value = FilterMessageOption.all; } - void markAsReadMailboxAction() { + void markAsReadMailboxAction(BuildContext context) { + final session = sessionCurrent; final currentAccountId = accountId.value; final mailboxId = selectedMailbox.value?.id; - final mailboxName = selectedMailbox.value?.name; final countEmailsUnread = selectedMailbox.value?.unreadEmails?.value.value ?? 0; - if (currentAccountId != null && mailboxId != null && mailboxName != null) { - markAsReadMailbox(currentAccountId, mailboxId, mailboxName, countEmailsUnread.toInt()); + if (session != null && currentAccountId != null && mailboxId != null) { + markAsReadMailbox( + session, + currentAccountId, + mailboxId, + selectedMailbox.value?.getDisplayName(context) ?? '', + countEmailsUnread.toInt() + ); } } void markAsReadMailbox( - AccountId accountId, - MailboxId mailboxId, - MailboxName mailboxName, - int totalEmailsUnread + Session session, + AccountId accountId, + MailboxId mailboxId, + String mailboxDisplayName, + int totalEmailsUnread ) { consumeState(_markAsMailboxReadInteractor.execute( - accountId, - mailboxId, - mailboxName, - totalEmailsUnread, - _progressStateController)); + session, + accountId, + mailboxId, + mailboxDisplayName, + totalEmailsUnread, + _progressStateController)); } void _markAsReadMailboxSuccess(Success success) { @@ -1219,21 +1339,17 @@ class MailboxDashBoardController extends ReloadableController { if (success is MarkAsMailboxReadAllSuccess) { if (currentContext != null && currentOverlayContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - widthToast: _responsiveUtils.isDesktop(currentContext!) ? 360 : null, - message: AppLocalizations.of(currentContext!) - .toastMessageMarkAsMailboxReadSuccess(success.mailboxName.name), - icon: _imagePaths.icReadToast); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).toastMessageMarkAsMailboxReadSuccess(success.mailboxDisplayName), + leadingSVGIcon: _imagePaths.icReadToast); } } else if (success is MarkAsMailboxReadHasSomeEmailFailure) { if (currentContext != null && currentOverlayContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - widthToast: _responsiveUtils.isDesktop(currentContext!) ? 360 : null, - message: AppLocalizations.of(currentContext!) - .toastMessageMarkAsMailboxReadHasSomeEmailFailure(success.mailboxName.name, success.countEmailsRead), - icon: _imagePaths.icReadToast); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).toastMessageMarkAsMailboxReadHasSomeEmailFailure(success.mailboxDisplayName, success.countEmailsRead), + leadingSVGIcon: _imagePaths.icReadToast); } } } @@ -1242,13 +1358,16 @@ class MailboxDashBoardController extends ReloadableController { viewStateMarkAsReadMailbox.value = Right(UIState.idle); } - void goToComposer(ComposerArguments arguments) { - if (BuildUtils.isWeb) { + void goToComposer(ComposerArguments arguments) async { + if (PlatformInfo.isWeb) { if (composerOverlayState.value == ComposerOverlayState.inActive) { openComposerOverlay(arguments); } } else { - push(AppRoutes.composer, arguments: arguments); + final result = await push(AppRoutes.composer, arguments: arguments); + if (result is SendingEmailArguments) { + handleSendEmailAction(result); + } } } @@ -1265,36 +1384,38 @@ class MailboxDashBoardController extends ReloadableController { } void goToSettings() async { - if (isDrawerOpen) { - closeMailboxMenuDrawer(); - } - final result = await push(AppRoutes.settings, - arguments: ManageAccountArguments(sessionCurrent)); + closeMailboxMenuDrawer(); + final result = await push( + AppRoutes.settings, + arguments: ManageAccountArguments(sessionCurrent) + ); if (result is VacationResponse) { vacationResponse.value = result; + dispatchMailboxUIAction(RefreshChangeMailboxAction(null)); } } - void selectQuickSearchFilter({ - required QuickSearchFilter quickSearchFilter, - bool fromSuggestionBox = false, - }) => searchController.selectQuickSearchFilter( - quickSearchFilter: quickSearchFilter, - userProfile: userProfile.value!, - fromSuggestionBox: fromSuggestionBox, - ); + void selectQuickSearchFilter(QuickSearchFilter filter) { + return searchController.selectQuickSearchFilter(filter, userProfile.value!); + } - bool checkQuickSearchFilterSelected({ - required QuickSearchFilter quickSearchFilter, - bool fromSuggestionBox = false, - }) => searchController.checkQuickSearchFilterSelected( - quickSearchFilter: quickSearchFilter, - userProfile: userProfile.value!, - fromSuggestionBox: fromSuggestionBox, - ); + void addFilterToSuggestionForm(QuickSearchFilter filter) { + searchController.addFilterToSuggestionForm(filter); + } - Future> quickSearchEmails() => searchController.quickSearchEmails(accountId: accountId.value!); + Future> quickSearchEmails(String query) async { + if (sessionCurrent != null && accountId.value != null && userProfile.value != null) { + return searchController.quickSearchEmails( + session: sessionCurrent!, + accountId: accountId.value!, + userProfile: userProfile.value!, + query: query + ); + } else { + return []; + } + } void addDownloadTask(DownloadTaskState task) { downloadController.addDownloadTask(task); @@ -1336,10 +1457,9 @@ class MailboxDashBoardController extends ReloadableController { void _handleUpdateVacationSuccess(UpdateVacationSuccess success) { if (success.listVacationResponse.isNotEmpty) { if (currentContext != null && currentOverlayContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - message: AppLocalizations.of(currentContext!).yourVacationResponderIsDisabledSuccessfully, - icon: _imagePaths.icChecked); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).yourVacationResponderIsDisabledSuccessfully); } vacationResponse.value = success.listVacationResponse.first; log('MailboxDashBoardController::_handleUpdateVacationSuccess(): $vacationResponse'); @@ -1348,27 +1468,57 @@ class MailboxDashBoardController extends ReloadableController { void selectQuickSearchFilterAction(QuickSearchFilter filter) { log('MailboxDashBoardController::selectQuickSearchFilterAction(): filter: $filter'); - selectQuickSearchFilter(quickSearchFilter: filter); + selectQuickSearchFilter(filter); dispatchAction(StartSearchEmailAction()); } - void selectReceiveTimeQuickSearchFilter(EmailReceiveTimeType? emailReceiveTimeType) { + void selectReceiveTimeQuickSearchFilter(BuildContext context, EmailReceiveTimeType receiveTime) { + log('MailboxDashBoardController::selectReceiveTimeQuickSearchFilter():receiveTime: $receiveTime'); popBack(); - if (emailReceiveTimeType != null) { - searchController.updateFilterEmail(emailReceiveTimeType: emailReceiveTimeType); + if (receiveTime == EmailReceiveTimeType.customRange) { + searchController.showMultipleViewDateRangePicker( + context, + searchController.startDateFiltered, + searchController.endDateFiltered, + onCallbackAction: (startDate, endDate) { + dispatchAction(SelectDateRangeToAdvancedSearch(startDate, endDate)); + searchController.updateFilterEmail( + emailReceiveTimeType: receiveTime, + startDateOption: optionOf(startDate?.toUTCDate()), + endDateOption: optionOf(startDate?.toUTCDate()), + beforeOption: const None() + ); + dispatchAction(StartSearchEmailAction()); + } + ); } else { - searchController.updateFilterEmail(emailReceiveTimeType: EmailReceiveTimeType.allTime); - } - searchController.setEmailReceiveTimeType(emailReceiveTimeType); - searchController.updateFilterEmail(); - if (searchController.searchQuery == null){ - searchController.updateFilterEmail(text: SearchQuery.initial()); + dispatchAction(ClearDateRangeToAdvancedSearch(receiveTime)); + searchController.updateFilterEmail( + emailReceiveTimeType: receiveTime, + startDateOption: const None(), + endDateOption: const None(), + beforeOption: const None() + ); + dispatchAction(StartSearchEmailAction()); } - dispatchAction(StartSearchEmailAction()); } - bool get isMailboxTrash => selectedMailbox.value?.isTrash == true; + bool isEmptyTrashBannerEnabledOnWeb(BuildContext context) { + return selectedMailbox.value != null && + selectedMailbox.value!.isTrash && + selectedMailbox.value!.countTotalEmails > 0 && + !searchController.isSearchActive() && + _responsiveUtils.isWebDesktop(context); + } + + bool isEmptyTrashBannerEnabledOnMobile(BuildContext context) { + return selectedMailbox.value != null && + selectedMailbox.value!.isTrash && + selectedMailbox.value!.countTotalEmails > 0 && + !searchController.isSearchActive() && + !_responsiveUtils.isWebDesktop(context); + } void emptyTrashAction(BuildContext context) { dispatchAction(EmptyTrashAction(context)); @@ -1401,11 +1551,11 @@ class MailboxDashBoardController extends ReloadableController { } catch (e) { logError('MailboxDashBoardController::_handleRefreshActionWhenBackToApp(): $e'); } - if (_getEmailStateToRefreshInteractor != null) { - consumeState(_getEmailStateToRefreshInteractor!.execute()); + if (_getEmailStateToRefreshInteractor != null && accountId.value != null && sessionCurrent != null) { + consumeState(_getEmailStateToRefreshInteractor!.execute(accountId.value!, sessionCurrent!.username)); } - if (_getMailboxStateToRefreshInteractor != null) { - consumeState(_getMailboxStateToRefreshInteractor!.execute()); + if (_getMailboxStateToRefreshInteractor != null && accountId.value != null && sessionCurrent != null) { + consumeState(_getMailboxStateToRefreshInteractor!.execute(accountId.value!, sessionCurrent!.username)); } } @@ -1416,8 +1566,8 @@ class MailboxDashBoardController extends ReloadableController { } catch (e) { logError('MailboxDashBoardController::_deleteEmailStateToRefreshAction(): $e'); } - if (_deleteEmailStateToRefreshInteractor != null) { - consumeState(_deleteEmailStateToRefreshInteractor!.execute()); + if (_deleteEmailStateToRefreshInteractor != null && accountId.value != null && sessionCurrent != null) { + consumeState(_deleteEmailStateToRefreshInteractor!.execute(accountId.value!, sessionCurrent!.username)); } } @@ -1428,8 +1578,8 @@ class MailboxDashBoardController extends ReloadableController { } catch (e) { logError('MailboxDashBoardController::_deleteMailboxStateToRefreshAction(): $e'); } - if (_deleteMailboxStateToRefreshInteractor != null) { - consumeState(_deleteMailboxStateToRefreshInteractor!.execute()); + if (_deleteMailboxStateToRefreshInteractor != null && accountId.value != null && sessionCurrent != null) { + consumeState(_deleteMailboxStateToRefreshInteractor!.execute(accountId.value!, sessionCurrent!.username)); } } @@ -1439,9 +1589,10 @@ class MailboxDashBoardController extends ReloadableController { _handleMessageFromNotification(response?.payload); } - void _handleMessageFromNotification(String? payload, {bool onForeground = true}) { + void _handleMessageFromNotification(String? payload, {bool onForeground = true}) async { + await LocalNotificationManager.instance.removeNotificationBadgeForIOS(); log('MailboxDashBoardController::_handleMessageFromNotification():payload: $payload'); - if (payload == null) { + if (payload == null || payload.isEmpty) { dispatchRoute(DashboardRoutes.thread); return; } @@ -1465,17 +1616,18 @@ class MailboxDashBoardController extends ReloadableController { void _handleNotificationMessageFromNewState(jmap.State newState, {bool onForeground = true}) { if (onForeground) { - _openInboxMailbox(); + _openInboxMailboxFromNotification(); } } void _handleNotificationMessageFromEmailId(EmailId emailId, {bool onForeground = true}) { final currentAccountId = accountId.value; - if (currentAccountId != null) { + final session = sessionCurrent; + if (currentAccountId != null && session != null) { if (onForeground) { _showWaitingView(); } - _getPresentationEmailFromEmailIdAction(emailId, currentAccountId); + _getPresentationEmailFromEmailIdAction(emailId, currentAccountId, session); } else { dispatchRoute(DashboardRoutes.thread); } @@ -1486,18 +1638,19 @@ class MailboxDashBoardController extends ReloadableController { dispatchRoute( DashboardRoutes.waiting); } - void _openInboxMailbox() { + void _openInboxMailboxFromNotification() { popAllRouteIfHave(); dispatchMailboxUIAction(SelectMailboxDefaultAction()); dispatchRoute(DashboardRoutes.thread); } - void _getPresentationEmailFromEmailIdAction(EmailId emailId, AccountId accountId) { + void _getPresentationEmailFromEmailIdAction(EmailId emailId, AccountId accountId, Session session) { log('MailboxDashBoardController:_getPresentationEmailFromEmailIdAction:emailId: $emailId'); consumeState(_getEmailByIdInteractor.execute( + session, accountId, emailId, - properties: ThreadConstants.propertiesDefault + properties: EmailUtils.getPropertiesForEmailGetMethod(session, accountId) )); } @@ -1518,7 +1671,7 @@ class MailboxDashBoardController extends ReloadableController { void handleOnForegroundGained() { log('MailboxDashBoardController::handleOnForegroundGained():'); - if (!BuildUtils.isWeb) { + if (PlatformInfo.isMobile) { _updateTheme(); } refreshActionWhenBackToApp(); @@ -1535,21 +1688,29 @@ class MailboxDashBoardController extends ReloadableController { emailsInCurrentMailbox.value = newEmailList; } - void openMailboxAction(BuildContext context, PresentationMailbox presentationMailbox) { - dispatchAction(OpenMailboxAction(context, presentationMailbox)); + void openMailboxAction(PresentationMailbox presentationMailbox) { + dispatchMailboxUIAction(OpenMailboxAction(presentationMailbox)); } bool get enableSpamReport => spamReportController.enableSpamReport; - void showSpamReportBanner() { - if (spamReportController.enableSpamReport) { - spamReportController.getUnreadSpamMailboxAction(accountId.value!); + void getSpamReportBanner() { + if (spamReportController.enableSpamReport && + sessionCurrent != null && + accountId.value != null) { + spamReportController.getSpamMailboxAction(sessionCurrent!, accountId.value!); + } + } + + void refreshSpamReportBanner() { + if (spamReportController.enableSpamReport && sessionCurrent != null && accountId.value != null) { + spamReportController.getSpamMailboxCached(accountId.value!, sessionCurrent!.username); } } void storeSpamReportStateAction() { - final _storeSpamReportState = enableSpamReport ? SpamReportState.disabled : SpamReportState.enabled; - spamReportController.storeSpamReportStateAction(_storeSpamReportState); + final storeSpamReportState = enableSpamReport ? SpamReportState.disabled : SpamReportState.enabled; + spamReportController.storeSpamReportStateAction(storeSpamReportState); } void onDragMailbox(bool isDragging) { @@ -1594,13 +1755,11 @@ class MailboxDashBoardController extends ReloadableController { } void _showToastSendMessageFailure(String message) { - if (currentOverlayContext != null) { - _appToast.showToastWithIcon( + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( currentOverlayContext!, - textColor: AppColor.toastErrorBackgroundColor, - message: message, - icon: _imagePaths.icSendToast - ); + message, + leadingSVGIcon: _imagePaths.icSendSuccessToast); } } @@ -1645,11 +1804,278 @@ class MailboxDashBoardController extends ReloadableController { clearState(); } - + + void handleSendEmailAction(SendingEmailArguments arguments) { + switch(arguments.actionMode) { + case ComposeActionMode.pushQueue: + _handleStoreSendingEmail( + arguments.session, + arguments.accountId, + arguments.emailRequest, + arguments.mailboxRequest + ); + break; + case ComposeActionMode.editQueue: + _handleUpdateSendingEmail( + arguments.session, + arguments.accountId, + arguments.emailRequest, + arguments.mailboxRequest + ); + break; + case ComposeActionMode.sent: + consumeState(_sendEmailInteractor.execute( + arguments.session, + arguments.accountId, + arguments.emailRequest, + mailboxRequest: arguments.mailboxRequest + )); + break; + } + } + + void _handleStoreSendingEmail( + Session session, + AccountId accountId, + EmailRequest emailRequest, + CreateNewMailboxRequest? mailboxRequest, + ) { + log('MailboxDashBoardController::_handleStoreSendingEmail:'); + final sendingEmail = emailRequest.toSendingEmail(_uuid.v1(), mailboxRequest: mailboxRequest); + consumeState(_storeSendingEmailInteractor.execute( + accountId, + session.username, + sendingEmail + )); + } + + void _handleUpdateSendingEmail( + Session session, + AccountId accountId, + EmailRequest emailRequest, + CreateNewMailboxRequest? mailboxRequest, + ) { + log('MailboxDashBoardController::_handleUpdateSendingEmail:'); + final storedSendingId = emailRequest.storedSendingId; + if (storedSendingId != null) { + final sendingEmail = emailRequest.toSendingEmail(storedSendingId, mailboxRequest: mailboxRequest); + consumeState(_updateSendingEmailInteractor.execute( + accountId, + session.username, + sendingEmail + )); + } else { + logError('MailboxDashBoardController::_handleUpdateSendingEmail(): StoredSendingId is null'); + _handleStoreSendingEmail( + session, + accountId, + emailRequest, + mailboxRequest + ); + } + } + + void _handleStoreSendingEmailSuccess(StoreSendingEmailSuccess success) { + addSendingEmailToSendingQueue(success.sendingEmail); + getAllSendingEmails(); + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).messageHasBeenSavedToTheSendingQueue, + leadingSVGIconColor: Colors.white, + leadingSVGIcon: _imagePaths.icEmail); + } + } + + void _handleUpdateSendingEmailSuccess(UpdateSendingEmailSuccess success) async { + await WorkManagerController().cancelByUniqueId(success.newSendingEmail.sendingId); + addSendingEmailToSendingQueue(success.newSendingEmail); + getAllSendingEmails(); + } + + void addSendingEmailToSendingQueue(SendingEmail sendingEmail) async { + log('MailboxDashBoardController::addSendingEmailToSendingQueue():sendingEmail: $sendingEmail'); + final work = OneTimeWorkRequest( + uniqueId: PlatformInfo.isAndroid + ? sendingEmail.sendingId + : WorkerType.sendingEmail.iOSUniqueId, + taskId: sendingEmail.sendingId, + tag: WorkerType.sendingEmail.name, + inputData: sendingEmail.toJson() + ..addAll({ + WorkManagerConstants.workerTypeKey: WorkerType.sendingEmail.name + }), + initialDelay: const Duration(milliseconds: WorkManagerConstants.delayTime), + backoffPolicy: work_manager.BackoffPolicy.linear, + backoffPolicyDelay: const Duration(milliseconds: WorkManagerConstants.delayTime), + constraints: work_manager.Constraints(networkType: work_manager.NetworkType.connected) + ); + + await WorkManagerController().enqueue(work); + } + + void getAllSendingEmails() { + if (accountId.value != null && sessionCurrent != null) { + consumeState(_getAllSendingEmailInteractor.execute( + accountId.value!, + sessionCurrent!.username + )); + } + } + + void _handleGetAllSendingEmailsSuccess(GetAllSendingEmailSuccess success) async { + listSendingEmails.value = success.sendingEmails; + + if (listSendingEmails.isEmpty && dashboardRoute.value == DashboardRoutes.sendingQueue) { + openDefaultMailbox(); + } + } + + void openDefaultMailbox() { + dispatchRoute(DashboardRoutes.thread); + dispatchMailboxUIAction(SelectMailboxDefaultAction()); + } + + void _storeSessionAction(Session session) { + consumeState(_storeSessionInteractor.execute(session)); + } + + void emptySpamFolderAction({Function? onCancelSelectionEmail, MailboxId? spamFolderId}) { + onCancelSelectionEmail?.call(); + + final spamMailboxId = spamFolderId ?? mapDefaultMailboxIdByRole[PresentationMailbox.roleSpam]; + if (sessionCurrent != null && accountId.value != null && spamMailboxId != null) { + consumeState( + _emptySpamFolderInteractor.execute( + sessionCurrent!, + accountId.value!, + spamMailboxId + ) + ); + } + } + + void _emptySpamFolderSuccess(EmptySpamFolderSuccess success) { + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).toast_message_empty_trash_folder_success); + } + } + + bool isEmptySpamBannerEnabledOnWeb(BuildContext context) { + return selectedMailbox.value != null && + selectedMailbox.value!.isSpam && + selectedMailbox.value!.countTotalEmails > 0 && + !searchController.isSearchActive() && + _responsiveUtils.isWebDesktop(context); + } + + bool isEmptySpamBannerEnabledOnMobile(BuildContext context) { + return selectedMailbox.value != null && + selectedMailbox.value!.isSpam && + selectedMailbox.value!.countTotalEmails > 0 && + !searchController.isSearchActive() && + !_responsiveUtils.isWebDesktop(context); + } + + void openDialogEmptySpamFolder(BuildContext context) { + final spamMailbox = selectedMailbox.value; + if (spamMailbox == null || !spamMailbox.isSpam) { + logError('MailboxDashBoardController::openDialogEmptySpamFolder: Selected mailbox is not spam'); + return; + + } + if (_responsiveUtils.isScreenWithShortestSide(context)) { + (ConfirmationDialogActionSheetBuilder(context) + ..messageText(AppLocalizations.of(context).emptySpamMessageDialog) + ..onCancelAction(AppLocalizations.of(context).cancel, popBack) + ..onConfirmAction(AppLocalizations.of(context).delete_all, () { + popBack(); + if (spamMailbox.countTotalEmails > 0) { + emptySpamFolderAction(spamFolderId: spamMailbox.id); + } else { + _appToast.showToastWarningMessage( + context, + AppLocalizations.of(context).noEmailInYourCurrentMailbox + ); + } + })) + .show(); + } else { + showDialog( + context: context, + barrierColor: AppColor.colorDefaultCupertinoActionSheet, + builder: (context) => PointerInterceptor(child: (ConfirmDialogBuilder(_imagePaths) + ..key(const Key('confirm_dialog_empty_spam')) + ..title(AppLocalizations.of(context).emptySpamFolder) + ..content(AppLocalizations.of(context).emptySpamMessageDialog) + ..addIcon(SvgPicture.asset(_imagePaths.icRemoveDialog, fit: BoxFit.fill)) + ..colorConfirmButton(AppColor.colorConfirmActionDialog) + ..styleTextConfirmButton(const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: AppColor.colorActionDeleteConfirmDialog)) + ..onCloseButtonAction(popBack) + ..onConfirmButtonAction(AppLocalizations.of(context).delete_all, () { + popBack(); + if (spamMailbox.countTotalEmails > 0) { + emptySpamFolderAction(spamFolderId: spamMailbox.id); + } else { + _appToast.showToastWarningMessage( + context, + AppLocalizations.of(context).noEmailInYourCurrentMailbox + ); + } + }) + ..onCancelButtonAction(AppLocalizations.of(context).cancel, popBack) + ).build()) + ); + } + } + + void refreshMailboxAction() async { + refreshingMailboxState.value = Right(RefreshAllEmailLoading()); + await Future.delayed(const Duration(milliseconds: 500)); + dispatchAction(RefreshAllEmailAction()); + } + + void selectAllEmailAction() { + dispatchAction(SelectionAllEmailAction()); + } + + String get baseDownloadUrl => sessionCurrent?.getDownloadUrl(jmapUrl: dynamicUrlInterceptors.jmapUrl) ?? ''; + + void redirectToInboxAction() { + log('MailboxDashBoardController::redirectToInboxAction:'); + if (dashboardRoute.value == DashboardRoutes.emailDetailed) { + dispatchEmailUIAction(CloseEmailDetailedViewAction()); + } + + final inboxId = getMailboxIdByRole(PresentationMailbox.roleInbox); + if (inboxId == null) return; + + final inboxPresentation = mapMailboxById[inboxId]; + if (inboxPresentation == null) return; + + openMailboxAction(inboxPresentation); + } + + bool get isDraggableAppActive => draggableAppState.value == DraggableAppState.active; + + void enableDraggableApp() { + draggableAppState.value = DraggableAppState.active; + } + + void disableDraggableApp() { + draggableAppState.value = DraggableAppState.inActive; + } + @override void onClose() { _emailReceiveManager.closeEmailReceiveManagerStream(); - _emailReceiveManagerStreamSubscription.cancel(); + _emailAddressStreamSubscription.cancel(); + _emailContentStreamSubscription.cancel(); _fileReceiveManagerStreamSubscription.cancel(); _progressStateController.close(); _refreshActionEventController.close(); diff --git a/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart index 8ceb89a799..e09a493574 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/search_controller.dart @@ -1,17 +1,25 @@ import 'dart:async'; -import 'package:core/core.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator_property.dart'; -import 'package:model/model.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/email_filter_condition_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/base/mixin/date_range_picker_mixin.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_all_recent_search_latest_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/quick_search_email_state.dart'; @@ -27,7 +35,7 @@ import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_status.dart'; -class SearchController extends BaseController { +class SearchController extends BaseController with DateRangePickerMixin { final QuickSearchEmailInteractor _quickSearchEmailInteractor; final SaveRecentSearchInteractor _saveRecentSearchInteractor; final GetAllRecentSearchLatestInteractor _getAllRecentSearchLatestInteractor; @@ -38,10 +46,9 @@ class SearchController extends BaseController { final searchEmailFilter = SearchEmailFilter.initial().obs; final searchState = SearchState.initial().obs; final isAdvancedSearchViewOpen = false.obs; - final emailReceiveTimeType = Rxn(); + final listFilterOnSuggestionForm = RxList(); final simpleSearchIsActivated = RxBool(false); final advancedSearchIsActivated = RxBool(false); - final autoFocus = RxBool(true); SearchQuery? get searchQuery => searchEmailFilter.value.text; @@ -53,13 +60,7 @@ class SearchController extends BaseController { this._getAllRecentSearchLatestInteractor, ); - @override - void onInit() { - _registerSearchFocusListener(); - super.onInit(); - } - - selectOpenAdvanceSearch() { + void selectOpenAdvanceSearch() { isAdvancedSearchViewOpen.toggle(); } @@ -67,57 +68,110 @@ class SearchController extends BaseController { searchEmailFilter.value = SearchEmailFilter.initial(); } - void selectQuickSearchFilter({ - required QuickSearchFilter quickSearchFilter, - required UserProfile userProfile, - bool fromSuggestionBox = false, - }) { - final quickSearchFilterSelected = checkQuickSearchFilterSelected( - userProfile: userProfile, - quickSearchFilter: quickSearchFilter, - fromSuggestionBox: fromSuggestionBox, - ); + void selectQuickSearchFilter(QuickSearchFilter quickSearchFilter, UserProfile userProfile) { + final isFilterSelected = quickSearchFilter.isSelected(searchEmailFilter.value, userProfile); switch (quickSearchFilter) { case QuickSearchFilter.hasAttachment: - updateFilterEmail(hasAttachment: !quickSearchFilterSelected); + updateFilterEmail(hasAttachment: !isFilterSelected); return; case QuickSearchFilter.last7Days: - if (quickSearchFilterSelected) { - setEmailReceiveTimeType(null); - updateFilterEmail(emailReceiveTimeType: EmailReceiveTimeType.allTime); - } else { - setEmailReceiveTimeType(EmailReceiveTimeType.last7Days); - updateFilterEmail(emailReceiveTimeType: EmailReceiveTimeType.last7Days); - } + updateFilterEmail(emailReceiveTimeType: EmailReceiveTimeType.allTime); return; case QuickSearchFilter.fromMe: - quickSearchFilterSelected - ? searchEmailFilter.value.from.removeWhere((e) => e == userProfile.email) - : searchEmailFilter.value.from.add(userProfile.email); - updateFilterEmail(from: searchEmailFilter.value.from); + isFilterSelected + ? searchEmailFilter.value.from.removeWhere((e) => e == userProfile.email) + : searchEmailFilter.value.from.add(userProfile.email); + updateFilterEmail(fromOption: Some(searchEmailFilter.value.from)); return; } } - Future> quickSearchEmails({required AccountId accountId}) async { - return await _quickSearchEmailInteractor - .execute(accountId, - limit: UnsignedInt(5), - sort: {}..add( - EmailComparator(EmailComparatorProperty.receivedAt) - ..setIsAscending(false)), - filter: searchEmailFilter.value.mappingToEmailFilterCondition(), - properties: ThreadConstants.propertiesQuickSearch) - .then((result) => result.fold( - (failure) => [], - (success) => success is QuickSearchEmailSuccess - ? success.emailList - : [])); + void addFilterToSuggestionForm(QuickSearchFilter filter) { + if (listFilterOnSuggestionForm.contains(filter)) { + listFilterOnSuggestionForm.remove(filter); + } else { + listFilterOnSuggestionForm.add(filter); + } + } + + Future> quickSearchEmails({ + required Session session, + required AccountId accountId, + required String query, + required UserProfile userProfile, + }) async { + return await _quickSearchEmailInteractor.execute( + session, + accountId, + limit: UnsignedInt(5), + sort: {}..add( + EmailComparator(EmailComparatorProperty.receivedAt) + ..setIsAscending(false)), + filter: _mappingToFilterOnSuggestionForm(userProfile: userProfile, query: query), + properties: ThreadConstants.propertiesQuickSearch + ).then((result) => result.fold( + (failure) => [], + (success) => success is QuickSearchEmailSuccess + ? success.emailList + : [] + )); + } + + Filter? _mappingToFilterOnSuggestionForm({required String query, required UserProfile userProfile}) { + log('SearchController::_mappingToFilterOnSuggestionForm():query: $query'); + final filterCondition = EmailFilterCondition( + text: query.isNotEmpty == true ? query : null, + after: listFilterOnSuggestionForm.contains(QuickSearchFilter.last7Days) + ? EmailReceiveTimeType.last7Days.toOldestUTCDate() + : null, + before: listFilterOnSuggestionForm.contains(QuickSearchFilter.last7Days) + ? EmailReceiveTimeType.last7Days.toLatestUTCDate() + : null, + hasAttachment: listFilterOnSuggestionForm.contains(QuickSearchFilter.hasAttachment) + ? true + : null, + from: listFilterOnSuggestionForm.contains(QuickSearchFilter.fromMe) + ? userProfile.email + : null + ); + + return filterCondition.hasCondition + ? filterCondition + : null; + } + + void applyFilterSuggestionToSearchFilter(UserProfile? userProfile) { + final receiveTime = listFilterOnSuggestionForm.contains(QuickSearchFilter.last7Days) + ? EmailReceiveTimeType.last7Days + : EmailReceiveTimeType.allTime; + + final hasAttachment = listFilterOnSuggestionForm.contains(QuickSearchFilter.hasAttachment) ? true : false; + + var listFromAddress = searchEmailFilter.value.from; + if (userProfile != null) { + if (listFilterOnSuggestionForm.contains(QuickSearchFilter.fromMe)) { + listFromAddress.add(userProfile.email); + } else { + listFromAddress.remove(userProfile.email); + } + } + + updateFilterEmail( + emailReceiveTimeType: receiveTime, + hasAttachment: hasAttachment, + fromOption: Some(listFromAddress) + ); + + clearFilterSuggestion(); + } + + void clearFilterSuggestion() { + listFilterOnSuggestionForm.clear(); } void updateFilterEmail({ - Set? from, + Option>? fromOption, Set? to, SearchQuery? text, Option? subjectOption, @@ -125,12 +179,12 @@ class SearchController extends BaseController { PresentationMailbox? mailbox, EmailReceiveTimeType? emailReceiveTimeType, bool? hasAttachment, - UTCDate? before, - UTCDate? startDate, - UTCDate? endDate, + Option? beforeOption, + Option? startDateOption, + Option? endDateOption }) { searchEmailFilter.value = searchEmailFilter.value.copyWith( - from: from, + fromOption: fromOption, to: to, text: text, subjectOption: subjectOption, @@ -138,26 +192,18 @@ class SearchController extends BaseController { mailbox: mailbox, emailReceiveTimeType: emailReceiveTimeType, hasAttachment: hasAttachment, - before: before, - startDate: startDate, - endDate: endDate, + beforeOption: beforeOption, + startDateOption: startDateOption, + endDateOption: endDateOption, ); searchEmailFilter.refresh(); } - void _registerSearchFocusListener() { - searchFocus.addListener(() { - final hasFocus = searchFocus.hasFocus; - final query = searchEmailFilter.value.text?.value; - log('SearchController::_registerSearchFocusListener(): hasFocus: $hasFocus | query: $query'); - if (!hasFocus && (query == null || query.isEmpty) && advancedSearchIsActivated.isFalse) { - updateFilterEmail(text: SearchQuery.initial()); - searchInputController.clear(); - clearSearchFilter(); - searchFocus.unfocus(); - } - }); - } + EmailReceiveTimeType get receiveTimeFiltered => searchEmailFilter.value.emailReceiveTimeType; + + DateTime? get startDateFiltered => searchEmailFilter.value.startDate?.value.toLocal(); + + DateTime? get endDateFiltered => searchEmailFilter.value.endDate?.value.toLocal(); bool isSearchActive() => searchState.value.searchStatus == SearchStatus.ACTIVE; @@ -171,41 +217,19 @@ class SearchController extends BaseController { void disableSimpleSearch() { updateFilterEmail(text: SearchQuery.initial()); _clearAllTextInputSimpleSearch(); + deactivateSimpleSearch(); hideSimpleSearchFormView(); } void clearTextSearch() { - updateFilterEmail(text: SearchQuery.initial()); searchInputController.clear(); searchFocus.requestFocus(); } - void onChangeTextSearch(String value) { - updateFilterEmail(text: SearchQuery(value)); - } - void updateTextSearch(String value) { searchInputController.text = value; } - bool checkQuickSearchFilterSelected({ - required QuickSearchFilter quickSearchFilter, - required UserProfile userProfile, - bool fromSuggestionBox = false, - }) { - switch (quickSearchFilter) { - case QuickSearchFilter.hasAttachment: - return searchEmailFilter.value.hasAttachment == true; - case QuickSearchFilter.last7Days: - if (emailReceiveTimeType.value != null) { - return true; - } - return searchEmailFilter.value.emailReceiveTimeType == EmailReceiveTimeType.last7Days; - case QuickSearchFilter.fromMe: - return searchEmailFilter.value.from.contains( userProfile.email) && searchEmailFilter.value.from.length == 1; - } - } - void saveRecentSearch(RecentSearch recentSearch) { consumeState(_saveRecentSearchInteractor.execute(recentSearch)); } @@ -220,11 +244,7 @@ class SearchController extends BaseController { : [])); } - void setEmailReceiveTimeType(EmailReceiveTimeType? receiveTimeType) { - emailReceiveTimeType.value = receiveTimeType; - } - - showAdvancedFilterView(BuildContext context) async { + void showAdvancedFilterView(BuildContext context) async { selectOpenAdvanceSearch(); if (_responsiveUtils.isMobile(context)) { await showAdvancedSearchFilterBottomSheet(context); @@ -259,7 +279,6 @@ class SearchController extends BaseController { void _clearAllTextInputSimpleSearch() { searchInputController.clear(); searchFocus.unfocus(); - emailReceiveTimeType.value = null; } void disableAllSearchEmail() { @@ -272,9 +291,6 @@ class SearchController extends BaseController { hideAdvancedSearchFormView(); } - @override - void onDone() {} - @override void onClose() { searchInputController.dispose(); diff --git a/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart b/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart index e9e958ca64..2ad681b616 100644 --- a/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart +++ b/lib/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart @@ -1,17 +1,24 @@ -import 'package:core/utils/app_logger.dart'; -import 'package:flutter/material.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox_filter_condition.dart'; import 'package:model/extensions/mailbox_extension.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/spam_report_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_number_of_unread_spam_emails_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_spam_mailbox_cached_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_spam_report_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/store_last_time_dismissed_spam_reported_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/store_spam_report_state.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_spam_mailbox_cached_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_spam_report_state_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/get_unread_spam_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/store_last_time_dismissed_spam_reported_interactor.dart'; @@ -23,48 +30,76 @@ class SpamReportController extends BaseController { final GetUnreadSpamMailboxInteractor _getNumberOfUnreadSpamEmailsInteractor; final StoreSpamReportStateInteractor _storeSpamReportStateInteractor; final GetSpamReportStateInteractor _getSpamReportStateInteractor; + final GetSpamMailboxCachedInteractor _getSpamMailboxCachedInteractor; final _presentationSpamMailbox = Rxn(); final _spamReportState = Rxn(SpamReportState.enabled); SpamReportController( - this._storeSpamReportInteractor, - this._getNumberOfUnreadSpamEmailsInteractor, - this._storeSpamReportStateInteractor, - this._getSpamReportStateInteractor); + this._storeSpamReportInteractor, + this._getNumberOfUnreadSpamEmailsInteractor, + this._storeSpamReportStateInteractor, + this._getSpamReportStateInteractor, + this._getSpamMailboxCachedInteractor + ); @override - void onDone() { - viewState.value.fold( - (failure) { - logError('SpamReportController::onDone(): failure: $failure'); - }, - (success) { - if(success is GetUnreadSpamMailboxSuccess){ - _presentationSpamMailbox.value = success.unreadSpamMailbox.toPresentationMailbox(); - log('SpamReportController::GetNumberOfUnreadSpamEmailsSuccess():success $success'); - } else if (success is StoreLastTimeDismissedSpamReportSuccess) { - _presentationSpamMailbox.value = null; - log('SpamReportController::StoreLastTimeDismissedSpamReportSuccess():success $success'); - } else if (success is GetSpamReportStateSuccess) { - _spamReportState.value = success.spamReportState; - log('SpamReportController::GetSpamReportStateSuccess():success $success'); - } else if (success is StoreSpamReportStateSuccess) { - _spamReportState.value = success.spamReportState; - log('SpamReportController::StoreSpamReportStateSuccess():success $success'); - } - }, - ); + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetUnreadSpamMailboxSuccess){ + _presentationSpamMailbox.value = success.unreadSpamMailbox.toPresentationMailbox(); + } else if (success is StoreLastTimeDismissedSpamReportSuccess) { + _presentationSpamMailbox.value = null; + } else if (success is GetSpamReportStateSuccess) { + _spamReportState.value = success.spamReportState; + } else if (success is StoreSpamReportStateSuccess) { + _spamReportState.value = success.spamReportState; + } else if (success is GetSpamMailboxCachedSuccess) { + _presentationSpamMailbox.value = success.spamMailbox.toPresentationMailbox(); + } } - - void dismissSpamReportAction() { - _storeLastTimeDismissedSpamReportedAction(); + + @override + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is GetUnreadSpamMailboxFailure || + failure is GetSpamMailboxCachedFailure || + failure is InvalidSpamReportCondition) { + _presentationSpamMailbox.value = null; + } + } + + void dismissSpamReportAction(BuildContext context) { + if (Get.isRegistered()) { + final mailboxDashBoardController = Get.find(); + final spamMailbox = _presentationSpamMailbox.value; + final session = mailboxDashBoardController.sessionCurrent; + final accountId = mailboxDashBoardController.accountId.value; + + if (spamMailbox != null && session != null && accountId != null) { + _storeLastTimeDismissedSpamReportedAction(); + + mailboxDashBoardController.markAsReadMailbox( + session, + accountId, + spamMailbox.id, + spamMailbox.getDisplayName(context), + spamMailbox.unreadEmails?.value.value.toInt() ?? 0 + ); + _presentationSpamMailbox.value = null; + } + } } - void getUnreadSpamMailboxAction(AccountId accountId) { - final _mailboxFilterCondition = MailboxFilterCondition(role: Role('Spam')); - getSpamReportStateAction(); - consumeState(_getNumberOfUnreadSpamEmailsInteractor.execute(accountId,mailboxFilterCondition: _mailboxFilterCondition)); + void getSpamMailboxAction(Session session, AccountId accountId) { + consumeState(_getNumberOfUnreadSpamEmailsInteractor.execute( + session, + accountId, + mailboxFilterCondition: MailboxFilterCondition(role: Role('Spam')))); + } + + void getSpamMailboxCached(AccountId accountId, UserName userName) { + consumeState(_getSpamMailboxCachedInteractor.execute(accountId, userName)); } void _storeLastTimeDismissedSpamReportedAction() { @@ -73,14 +108,14 @@ class SpamReportController extends BaseController { bool get notShowSpamReportBanner => _presentationSpamMailbox.value == null; - int get numberOfUnreadSpamEmails => (_presentationSpamMailbox.value?.unreadEmails?.value.value ?? 0).toInt(); + String get numberOfUnreadSpamEmails => _presentationSpamMailbox.value?.countUnReadEmailsAsString ?? ''; bool get enableSpamReport => _spamReportState.value == SpamReportState.enabled; - void openMailbox(BuildContext context) { - final _mailboxDashBoardController = Get.find(); - dismissSpamReportAction(); - _mailboxDashBoardController.openMailboxAction(context, _presentationSpamMailbox.value!); + void openMailbox() { + final mailboxDashBoardController = Get.find(); + _storeLastTimeDismissedSpamReportedAction(); + mailboxDashBoardController.openMailboxAction(_presentationSpamMailbox.value!); } void storeSpamReportStateAction(SpamReportState spamReportState) { diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart index 3da04b2419..e1af349057 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart @@ -3,13 +3,14 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/responsive/responsive_widget.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:focus_detector/focus_detector.dart'; +import 'package:focus_detector_v2/focus_detector_v2.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_view.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/base_mailbox_dashboard_view.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_view.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/sending_queue_view.dart'; import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart'; class MailboxDashBoardView extends BaseMailboxDashBoardView { @@ -33,7 +34,7 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { child: Scaffold( drawerEnableOpenDragGesture: responsiveUtils.hasLeftMenuDrawerActive(context), body: Obx(() { - final bodyView = controller.searchController.isSearchEmailRunning + var bodyView = controller.searchController.isSearchEmailRunning ? EmailView() : bodyLandscapeTablet; @@ -54,6 +55,22 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { mobile: EmailView()); case DashboardRoutes.searchEmail: return SafeArea(child: SearchEmailView()); + case DashboardRoutes.sendingQueue: + bodyView = Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: ResponsiveUtils.defaultSizeLeftMenuMobile, + child: _buildScaffoldHaveDrawer(body: const SendingQueueView())), + Expanded(child: EmailView()), + ], + ); + return ResponsiveWidget( + responsiveUtils: responsiveUtils, + desktop: bodyView, + tabletLarge: bodyView, + landscapeTablet: bodyView, + mobile: _buildScaffoldHaveDrawer(body: const SendingQueueView())); case DashboardRoutes.waiting: return const Center( child: SizedBox( @@ -79,22 +96,31 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { body: body, drawer: ResponsiveWidget( responsiveUtils: responsiveUtils, - mobile: SizedBox(child: MailboxView(), width: double.infinity), + mobile: SizedBox( + width: double.infinity, + child: MailboxView() + ), landscapeMobile: SizedBox( - child: MailboxView(), - width: ResponsiveUtils.defaultSizeDrawer), + width: ResponsiveUtils.defaultSizeDrawer, + child: MailboxView() + ), tablet: SizedBox( - child: MailboxView(), - width: ResponsiveUtils.defaultSizeDrawer), + width: ResponsiveUtils.defaultSizeDrawer, + child: MailboxView() + ), landscapeTablet: SizedBox( - child: MailboxView(), - width: ResponsiveUtils.defaultSizeLeftMenuMobile), + width: ResponsiveUtils.defaultSizeLeftMenuMobile, + child: MailboxView() + ), tabletLarge: SizedBox( - child: MailboxView(), - width: ResponsiveUtils.defaultSizeLeftMenuMobile), + width: ResponsiveUtils.defaultSizeLeftMenuMobile, + child: MailboxView() + ), desktop: SizedBox( - child: MailboxView(), - width: ResponsiveUtils.defaultSizeLeftMenuMobile)), + width: ResponsiveUtils.defaultSizeLeftMenuMobile, + child: MailboxView() + ) + ), ); } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart index cfb5ea8bc8..8b298621a8 100644 --- a/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart +++ b/lib/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart @@ -3,7 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/base/widget/cupertino_loading_widget.dart'; +import 'package:tmail_ui_user/features/base/widget/popup_item_no_icon_widget.dart'; import 'package:tmail_ui_user/features/composer/presentation/composer_view_web.dart'; import 'package:tmail_ui_user/features/email/presentation/email_view.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; @@ -11,23 +12,22 @@ import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read import 'package:tmail_ui_user/features/mailbox/presentation/mailbox_view_web.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/base_mailbox_dashboard_view.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/app_grid_dashboard_controller.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/composer_overlay_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_overlay.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/icon_open_advanced_search_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/spam_report_banner_web_widget.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/banner_delete_all_spam_emails_styles.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/banner_empty_trash_styles.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/banner_delete_all_spam_emails_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/banner_empty_trash_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/spam_banner/spam_report_banner_web_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; -import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_warning_banner_widget.dart'; +import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_banner_widget.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_view.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_view.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; @@ -35,6 +35,7 @@ import 'package:tmail_ui_user/features/thread/presentation/thread_view.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; import 'widgets/app_dashboard/app_grid_dashboard_overlay.dart'; @@ -42,11 +43,6 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { MailboxDashBoardView({Key? key}) : super(key: key); - final SearchController searchController = Get.find(); - final AppGridDashboardController appGridDashboardController = Get.find(); - final mailBoxDashboardController = Get.find(); - final SpamReportController spamReportController = Get.find(); - @override Widget build(BuildContext context) { controller.hideMailboxMenuWhenScreenSizeChange(context); @@ -56,89 +52,123 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { ResponsiveWidget( responsiveUtils: responsiveUtils, desktop: Scaffold( - body: Container( - color: AppColor.colorBgDesktop, - child: Column(children: [ - Row(children: [ - Container( - width: ResponsiveUtils.defaultSizeMenu, - color: Colors.white, - padding: const EdgeInsets.only(left: 28), - alignment: Alignment.center, - height: 80, - child: Row(children: [ - (SloganBuilder(arrangedByHorizontal: true) - ..setSloganText(AppLocalizations.of(context).app_name) - ..setSloganTextAlign(TextAlign.center) - ..setSloganTextStyle(const TextStyle( - color: Colors.black, - fontSize: 20, - fontWeight: FontWeight.bold)) - ..setSizeLogo(24) - ..setLogo(imagePaths.icLogoTMail)) - .build(), + body: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: Container( + color: AppColor.colorBgDesktop, + child: Column(children: [ + Row(children: [ + Container( + width: ResponsiveUtils.defaultSizeMenu, + color: Colors.white, + padding: EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 0 : 28, + right: AppUtils.isDirectionRTL(context) ? 28 : 0, + ), + alignment: Alignment.center, + height: 80, + child: Row(children: [ + SloganBuilder( + sizeLogo: 24, + text: AppLocalizations.of(context).app_name, + textAlign: TextAlign.center, + textStyle: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold), + logoSVG: imagePaths.icTMailLogo, + onTapCallback: controller.redirectToInboxAction, + ), + Obx(() { + if (controller.appInformation.value != null) { + return Padding(padding: const EdgeInsets.only(top: 6), + child: Text( + 'v${controller.appInformation.value!.version}', + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 13, + color: AppColor.colorContentEmail, + fontWeight: FontWeight.w500), + )); + } else { + return const SizedBox.shrink(); + } + }), + ]) + ), + Expanded(child: Container( + color: Colors.white, + alignment: Alignment.center, + padding: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) ? 0 : 10, + left: AppUtils.isDirectionRTL(context) ? 10 : 0, + ), + height: 80, + child: _buildRightHeader(context))) + ]), + Expanded(child: Row(children: [ + Column(children: [ + _buildComposerButton(context), + Expanded(child: SizedBox( + width: ResponsiveUtils.defaultSizeMenu, + child: Obx(() { + if (controller.searchMailboxActivated.isTrue) { + return const SearchMailboxView( + backgroundColor: AppColor.colorBgDesktop + ); + } else { + return MailboxView(); + } + }) + )) + ]), + Expanded(child: Column(children: [ + const SpamReportBannerWebWidget(), + const QuotasBannerWidget(), + _buildVacationNotificationMessage(context), Obx(() { - if (controller.appInformation.value != null) { - return Padding(padding: const EdgeInsets.only(top: 6), - child: Text( - 'v.${controller.appInformation.value!.version}', - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 13, - color: AppColor.colorContentEmail, - fontWeight: FontWeight.w500), - )); + if (controller.isEmptyTrashBannerEnabledOnWeb(context)) { + return Padding( + padding: const EdgeInsetsDirectional.only( + top: BannerEmptyTrashStyles.webTopMargin, + end: BannerEmptyTrashStyles.webEndMargin + ), + child: BannerEmptyTrashWidget( + onTapAction: () => controller.emptyTrashAction(context) + ), + ); } else { return const SizedBox.shrink(); } }), - ]) - ), - Expanded(child: Container( - color: Colors.white, - alignment: Alignment.center, - padding: const EdgeInsets.only(right: 10), - height: 80, - child: _buildRightHeader(context))) - ]), - Expanded(child: Row(children: [ - Column(children: [ - _buildComposerButton(context), - Expanded(child: SizedBox( - child: Obx(() { - if (controller.searchMailboxActivated.isTrue) { - return const SearchMailboxView( - backgroundColor: AppColor.colorBgDesktop + Obx(() { + if (controller.isEmptySpamBannerEnabledOnWeb(context)) { + return Padding( + padding: const EdgeInsetsDirectional.only( + top: BannerDeleteAllSpamEmailsStyles.webTopMargin, + end: BannerDeleteAllSpamEmailsStyles.webEndMargin + ), + child: BannerDeleteAllSpamEmailsWidget( + onTapAction: () => controller.openDialogEmptySpamFolder(context) + ), ); } else { - return MailboxView(); + return const SizedBox.shrink(); } }), - width: ResponsiveUtils.defaultSizeMenu - )) - ]), - Expanded(child: Column(children: [ - const SpamReportBannerWebWidget(), - _buildEmptyTrashButton(context), - const QuotasWarningBannerWidget( - margin: EdgeInsets.only(right: 16, top: 8), - ), - _buildVacationNotificationMessage(context), - _buildListButtonQuickSearchFilter(context), - _buildMarkAsMailboxReadLoading(context), - Expanded(child: Obx(() { - switch(controller.dashboardRoute.value) { - case DashboardRoutes.thread: - return _buildThreadViewForWebDesktop(context); - case DashboardRoutes.emailDetailed: - return EmailView(); - default: - return const SizedBox.shrink(); - } - })) + _buildListButtonQuickSearchFilter(context), + _buildMarkAsMailboxReadLoading(context), + Expanded(child: Obx(() { + switch(controller.dashboardRoute.value) { + case DashboardRoutes.thread: + return _buildThreadViewForWebDesktop(context); + case DashboardRoutes.emailDetailed: + return EmailView(); + default: + return const SizedBox.shrink(); + } + })) + ])) ])) - ])) - ]), + ]), + ), ), ), tabletLarge: Obx(() { @@ -204,8 +234,8 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { key: controller.scaffoldKey, drawer: ResponsiveWidget( responsiveUtils: responsiveUtils, - mobile: SizedBox(child: MailboxView(), width: ResponsiveUtils.defaultSizeDrawer), - tabletLarge: SizedBox(child: MailboxView(), width: ResponsiveUtils.defaultSizeLeftMenuMobile), + mobile: SizedBox(width: ResponsiveUtils.defaultSizeDrawer, child: MailboxView()), + tabletLarge: SizedBox(width: ResponsiveUtils.defaultSizeLeftMenuMobile, child: MailboxView()), desktop: const SizedBox.shrink() ), body: body, @@ -214,7 +244,12 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { Widget _buildThreadViewForWebDesktop(BuildContext context) { return Container( - margin: const EdgeInsets.only(right: 16, top: 8, bottom: 16), + margin: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) ? 0 : 16, + left: AppUtils.isDirectionRTL(context) ? 16 : 0, + top: 8, + bottom: 16 + ), decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), border: Border.all(color: AppColor.colorBorderBodyThread, width: 1), @@ -257,55 +292,34 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { Widget _buildRightHeader(BuildContext context) { return LayoutBuilder(builder: (context, constraint) { return Row(children: [ - Container( + SizedBox( width: constraint.maxWidth / 2, height: 52, - color: Colors.transparent, - child: Obx(() { - if (searchController.isSearchActive()) { - return SearchInputFormWidget( - maxWidth: constraint.maxWidth / 2, - dashBoardController: controller, - imagePaths: imagePaths); - } else { - return PortalTarget( - visible: searchController.isAdvancedSearchViewOpen.isTrue, - portalFollower: PointerInterceptor( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => searchController.selectOpenAdvanceSearch()), - ), - child: PortalTarget( - visible: searchController.isAdvancedSearchViewOpen.isTrue, - anchor: const Aligned( - follower: Alignment.topRight, - target: Alignment.bottomRight, - widthFactor: 1, - backup: Aligned( - follower: Alignment.topRight, - target: Alignment.bottomRight, - widthFactor: 1, - ), - ), - portalFollower: AdvancedSearchFilterOverlay(maxWidth: constraint.maxWidth / 2), - child: SearchBarView(imagePaths, - hintTextSearch: AppLocalizations.of(context).search_emails, - onOpenSearchViewAction: controller.searchController.enableSearch, - heightSearchBar: 52, - radius: 12, - rightButton: IconOpenAdvancedSearchWidget(context)), - ), - ); - } - })), + child: SearchInputFormWidget() + ), const Spacer(), AppConfig.appGridDashboardAvailable ? Obx(() => PortalTarget( - visible: appGridDashboardController.isAppGridDashboardOverlayOpen.isTrue, + visible: controller.appGridDashboardController.isAppGridDashboardOverlayOpen.isTrue, portalFollower: GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => appGridDashboardController.toggleAppGridDashboard()), + onTap: () => controller.appGridDashboardController.toggleAppGridDashboard()), child: PortalTarget( + anchor: Aligned( + follower: AppUtils.isDirectionRTL(context) + ? Alignment.topLeft + : Alignment.topRight, + target: AppUtils.isDirectionRTL(context) + ? Alignment.bottomLeft + : Alignment.bottomRight + ), + portalFollower: Obx(() { + if (controller.appGridDashboardController.linagoraApplications.value != null) { + return AppDashboardOverlay(controller.appGridDashboardController.linagoraApplications.value!); + } + return const SizedBox.shrink(); + }), + visible: controller.appGridDashboardController.isAppGridDashboardOverlayOpen.isTrue, child: buildIconWeb( onTap: controller.showAppDashboardAction, splashRadius: 20, @@ -316,17 +330,6 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { fit: BoxFit.fill ), ), - anchor: const Aligned( - follower: Alignment.topRight, - target: Alignment.bottomRight - ), - portalFollower: Obx(() { - if (appGridDashboardController.linagoraApplications.value != null) { - return AppDashboardOverlay(appGridDashboardController.linagoraApplications.value!); - } - return const SizedBox.shrink(); - }), - visible: appGridDashboardController.isAppGridDashboardOverlayOpen.isTrue, ) ) ) @@ -360,81 +363,84 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { Widget _buildListButtonTopBar(BuildContext context) { return Row(children: [ - (ButtonBuilder(imagePaths.icRefresh) - ..key(const Key('button_reload_thread')) - ..decoration(const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(10)), - color: AppColor.colorButtonHeaderThread)) - ..paddingIcon(EdgeInsets.zero) - ..size(16) - ..radiusSplash(10) - ..padding(const EdgeInsets.symmetric(horizontal: 8, vertical: 8)) - ..onPressActionClick(() => controller.dispatchAction(RefreshAllEmailAction()))) - .build(), + Obx(() { + return controller.refreshingMailboxState.value.fold( + (failure) { + return TMailButtonWidget.fromIcon( + key: const Key('refresh_mailbox_button'), + icon: imagePaths.icRefresh, + borderRadius: 10, + iconSize: 16, + onTapActionCallback: controller.refreshMailboxAction, + ); + }, + (success) { + if (success is RefreshAllEmailLoading) { + return const TMailContainerWidget( + borderRadius: 10, + padding: EdgeInsetsDirectional.symmetric(vertical: 8, horizontal: 8.5), + child: CupertinoLoadingWidget(size: 16)); + } else { + return TMailButtonWidget.fromIcon( + key: const Key('refresh_mailbox_button'), + icon: imagePaths.icRefresh, + borderRadius: 10, + iconSize: 16, + onTapActionCallback: controller.refreshMailboxAction, + ); + } + } + ); + }), const SizedBox(width: 16), - (ButtonBuilder(imagePaths.icSelectAll) - ..key(const Key('button_select_all')) - ..decoration(const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(10)), - color: AppColor.colorButtonHeaderThread)) - ..paddingIcon(const EdgeInsets.only(right: 8)) - ..size(16) - ..radiusSplash(10) - ..padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 8)) - ..textStyle(const TextStyle(fontSize: 12, color: AppColor.colorTextButtonHeaderThread)) - ..onPressActionClick(() => controller.dispatchAction(SelectionAllEmailAction())) - ..text(AppLocalizations.of(context).select_all, isVertical: false)) - .build(), - if (mailBoxDashboardController.isAbleMarkAllAsRead()) + TMailButtonWidget( + key: const Key('select_all_emails_button'), + text: AppLocalizations.of(context).select_all, + icon: imagePaths.icSelectAll, + borderRadius: 10, + iconSize: 16, + padding: const EdgeInsetsDirectional.symmetric(horizontal: 12, vertical: 8), + onTapActionCallback: controller.selectAllEmailAction, + ), + if (controller.isAbleMarkAllAsRead()) Padding( - padding: const EdgeInsets.only(left: 16), - child: (ButtonBuilder(imagePaths.icMarkAllAsRead) - ..key(const Key('button_mark_all_as_read')) - ..decoration(const BoxDecoration( - borderRadius:BorderRadius.all(Radius.circular(10)), - color: AppColor.colorButtonHeaderThread)) - ..paddingIcon(const EdgeInsets.only(right: 8)) - ..size(16) - ..padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 8)) - ..radiusSplash(10) - ..textStyle(const TextStyle( - fontSize: 12, - color: AppColor.colorTextButtonHeaderThread)) - ..onPressActionClick(() => controller.markAsReadMailboxAction()) - ..text(AppLocalizations.of(context).mark_all_as_read, isVertical: false)) - .build(), + padding: const EdgeInsetsDirectional.only(start: 16), + child: TMailButtonWidget( + key: const Key('mark_as_read_emails_button'), + text: AppLocalizations.of(context).mark_all_as_read, + icon: imagePaths.icSelectAll, + borderRadius: 10, + iconSize: 16, + padding: const EdgeInsetsDirectional.symmetric(horizontal: 12, vertical: 8), + onTapActionCallback: () => controller.markAsReadMailboxAction(context), + ), ), const SizedBox(width: 16), - Obx(() => (ButtonBuilder(controller.filterMessageOption.value.getIconSelected(imagePaths)) - ..key(const Key('button_filter_messages')) - ..context(context) - ..decoration(BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - color: controller.filterMessageOption.value.getBackgroundColor())) - ..paddingIcon(const EdgeInsets.only(right: 8)) - ..size(16) - ..padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 8)) - ..radiusSplash(10) - ..textStyle(controller.filterMessageOption.value.getTextStyle()) - ..addIconAction(Padding( - padding: const EdgeInsets.only(left: 8), - child: SvgPicture.asset(imagePaths.icArrowDown, fit: BoxFit.fill))) - ..addOnPressActionWithPositionClick((position) => - controller.openPopupMenuAction( - context, - position, - popupMenuFilterEmailActionTile( - context, - controller.filterMessageOption.value, - (option) => controller.dispatchAction(FilterMessageAction(context, option)), - isSearchEmailRunning: searchController.isSearchEmailRunning - ) - ) + Obx(() => TMailButtonWidget( + key: const Key('filter_emails_button'), + text: controller.filterMessageOption.value == FilterMessageOption.all + ? AppLocalizations.of(context).filter_messages + : controller.filterMessageOption.value.getTitle(context), + icon: controller.filterMessageOption.value.getIconSelected(imagePaths), + borderRadius: 10, + iconSize: 16, + padding: const EdgeInsetsDirectional.symmetric(horizontal: 12, vertical: 8), + backgroundColor: controller.filterMessageOption.value.getBackgroundColor(), + textStyle: controller.filterMessageOption.value.getTextStyle(), + trailingIcon: imagePaths.icArrowDown, + onTapActionAtPositionCallback: (position) { + return controller.openPopupMenuAction( + context, + position, + popupMenuFilterEmailActionTile( + context, + controller.filterMessageOption.value, + (option) => controller.dispatchAction(FilterMessageAction(context, option)), + isSearchEmailRunning: controller.searchController.isSearchEmailRunning ) - ..text(controller.filterMessageOption.value == FilterMessageOption.all - ? AppLocalizations.of(context).filter_messages - : controller.filterMessageOption.value.getTitle(context), isVertical: false)) - .build()), + ); + }, + )), ]); } @@ -505,7 +511,11 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { return Obx(() { if (controller.vacationResponse.value?.vacationResponderIsValid == true) { return VacationNotificationMessageWidget( - margin: const EdgeInsets.only(top: 16, right: 16), + margin: EdgeInsets.only( + top: 16, + right: AppUtils.isDirectionRTL(context) ? 0 : 16, + left: AppUtils.isDirectionRTL(context) ? 16 : 0, + ), vacationResponse: controller.vacationResponse.value!, actionGotoVacationSetting: () => controller.goToVacationSetting(), actionEndNow: () => controller.disableVacationResponder()); @@ -524,7 +534,11 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { return Obx(() { if (supportListButtonQuickSearchFilter(context)) { return Padding( - padding: const EdgeInsets.only(right: 16, top: 16), + padding: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) ? 0 : 16, + left: AppUtils.isDirectionRTL(context) ? 16 : 0, + top: 16 + ), child: Row(children: QuickSearchFilter.values .map((filter) => _buildQuickSearchFilterButton(context, filter)) .toList() @@ -541,12 +555,16 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { QuickSearchFilter filter ) { return Obx(() { - final quickSearchFilterSelected = controller.checkQuickSearchFilterSelected( - quickSearchFilter: filter, + final isFilterSelected = filter.isSelected( + controller.searchController.searchEmailFilter.value, + controller.userProfile.value ); return Padding( - padding: const EdgeInsets.only(right: 8), + padding: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) ? 0 : 8, + left: AppUtils.isDirectionRTL(context) ? 8 : 0, + ), child: InkWell( onTap: () { if (filter != QuickSearchFilter.last7Days) { @@ -563,35 +581,40 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { screenSize.width - offset.dx, screenSize.height - offset.dy, ); - controller.openPopupMenuAction(context, position, - popupMenuEmailReceiveTimeType(context, - controller.searchController.emailReceiveTimeType.value, - (receiveTime) => controller.selectReceiveTimeQuickSearchFilter(receiveTime))); + controller.openPopupMenuAction( + context, + position, + popupMenuEmailReceiveTimeType( + context, + controller.searchController.receiveTimeFiltered, + onCallBack: (receiveTime) => controller.selectReceiveTimeQuickSearchFilter(context, receiveTime) + ) + ); } }, borderRadius: const BorderRadius.all(Radius.circular(10)), child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), - color: filter.getBackgroundColor(quickSearchFilterSelected: quickSearchFilterSelected)), + color: filter.getBackgroundColor(isFilterSelected: isFilterSelected)), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ SvgPicture.asset( - filter.getIcon( - imagePaths, - quickSearchFilterSelected: quickSearchFilterSelected), + filter.getIcon(imagePaths, isFilterSelected: isFilterSelected), width: 16, height: 16, fit: BoxFit.fill), const SizedBox(width: 4), Text( filter.getTitle( - context, - receiveTimeType: controller.searchController.emailReceiveTimeType.value), + context, + receiveTimeType: controller.searchController.receiveTimeFiltered, + startDate: controller.searchController.startDateFiltered, + endDate: controller.searchController.endDateFiltered + ), maxLines: 1, overflow: TextOverflow.ellipsis, - style: filter.getTextStyle( - quickSearchFilterSelected: quickSearchFilterSelected), + style: filter.getTextStyle(isFilterSelected: isFilterSelected), ), if (filter == QuickSearchFilter.last7Days) ... [ @@ -609,142 +632,55 @@ class MailboxDashBoardView extends BaseMailboxDashBoardView { } List popupMenuEmailReceiveTimeType( - BuildContext context, - EmailReceiveTimeType? receiveTimeSelected, - Function(EmailReceiveTimeType?)? onCallBack + BuildContext context, + EmailReceiveTimeType? receiveTimeSelected, + {Function(EmailReceiveTimeType)? onCallBack} ) { return EmailReceiveTimeType.values - .map((timeType) => PopupMenuItem( - padding: EdgeInsets.zero, - child: _receiveTimeTileAction( - context, - receiveTimeSelected, - timeType, - onCallBack))) - .toList(); - } - - Widget _receiveTimeTileAction( - BuildContext context, - EmailReceiveTimeType? receiveTimeSelected, - EmailReceiveTimeType receiveTimeType, - Function(EmailReceiveTimeType?)? onCallBack - ) { - return InkWell( - onTap: () => onCallBack?.call(receiveTimeType == receiveTimeSelected - ? null - : receiveTimeType), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - child: SizedBox( - width: 320, - child: Row(children: [ - Expanded(child: Text( - receiveTimeType.getTitle(context), - style: const TextStyle( - fontSize: 17, - color: Colors.black, - fontWeight: FontWeight.normal))), - if (receiveTimeType == receiveTimeSelected) - ...[ - const SizedBox(width: 12), - SvgPicture.asset( - imagePaths.icFilterSelected, - width: 24, - height: 24, - fit: BoxFit.fill), - ] - ]) - ), - ) - ); - } - - bool supportEmptyTrash(BuildContext context) { - return controller.isMailboxTrash - && !controller.searchController.isSearchActive() - && responsiveUtils.isWebDesktop(context); - } - - Widget _buildEmptyTrashButton(BuildContext context) { - return Obx(() { - if (supportEmptyTrash(context)) { - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(14)), - border: Border.all(color: AppColor.colorLineLeftEmailView), - color: Colors.white), - margin: const EdgeInsets.only(right: 16, top: 16), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row(children: [ - Padding( - padding: const EdgeInsets.only(right: 16), - child: SvgPicture.asset( - imagePaths.icDeleteTrash, - fit: BoxFit.fill)), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8), - child: Text( - AppLocalizations.of(context).message_delete_all_email_in_trash_button, - style: const TextStyle( - color: AppColor.colorContentEmail, - fontSize: 13, - fontWeight: FontWeight.w500))), - TextButton( - onPressed: () => controller.emptyTrashAction(context), - child: Text( - AppLocalizations.of(context).empty_trash_now, - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.w500, - color: AppColor.colorTextButton) - ) - ) - ] - ) - ) - ]), - ); - } else { - return const SizedBox.shrink(); - } - }); + .map((receiveTime) => PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemNoIconWidget( + receiveTime.getTitle(context), + svgIconSelected: imagePaths.icFilterSelected, + maxWidth: 320, + isSelected: receiveTimeSelected == receiveTime, + onCallbackAction: () => onCallBack?.call(receiveTime), + ))) + .toList(); } Widget _buildComposerButton(BuildContext context) { return Container( - padding: const EdgeInsets.all(16), - color: Colors.transparent, + padding: const EdgeInsetsDirectional.only( + start: 16, + end: 16, + top: 16, + bottom: 8 + ), width: ResponsiveUtils.defaultSizeMenu, alignment: Alignment.centerLeft, - child: (ButtonBuilder(imagePaths.icComposeWeb) - ..key(const Key('button_compose_email')) - ..decoration(BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: AppColor.colorTextButton, - boxShadow: const [ - BoxShadow( - blurRadius: 12.0, - color: AppColor.colorShadowComposerButton - ) - ])) - ..paddingIcon(const EdgeInsets.only(right: 8)) - ..iconColor(Colors.white) - ..size(24) - ..radiusSplash(10) - ..padding(const EdgeInsets.symmetric(vertical: 8)) - ..textStyle(const TextStyle( - fontSize: 15, - color: Colors.white, - fontWeight: FontWeight.w500 - )) - ..onPressActionClick(() => controller.goToComposer(ComposerArguments())) - ..text(AppLocalizations.of(context).compose, isVertical: false) - ).build() + child: TMailButtonWidget( + key: const Key('compose_email_button'), + text: AppLocalizations.of(context).compose, + icon: imagePaths.icComposeWeb, + borderRadius: 10, + iconSize: 24, + iconColor: Colors.white, + padding: const EdgeInsetsDirectional.symmetric(vertical: 8), + backgroundColor: AppColor.colorTextButton, + boxShadow: const [ + BoxShadow( + blurRadius: 12.0, + color: AppColor.colorShadowComposerButton + ) + ], + textStyle: const TextStyle( + fontSize: 15, + color: Colors.white, + fontWeight: FontWeight.w500 + ), + onTapActionCallback: () => controller.goToComposer(ComposerArguments()), + ), ); } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart b/lib/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart index 7860c49ab9..056cbe43f9 100644 --- a/lib/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart +++ b/lib/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart @@ -63,8 +63,8 @@ mixin FilterEmailPopupMenuMixin { width: 20, height: 20, fit: BoxFit.fill, - color: option != FilterMessageOption.starred - ? AppColor.colorTextButton + colorFilter: option != FilterMessageOption.starred + ? AppColor.colorTextButton.asFilter() : null), const SizedBox(width: 12), Expanded(child: Text( diff --git a/lib/features/mailbox_dashboard/presentation/model/dashboard_routes.dart b/lib/features/mailbox_dashboard/presentation/model/dashboard_routes.dart index 82d208a900..65b7a31203 100644 --- a/lib/features/mailbox_dashboard/presentation/model/dashboard_routes.dart +++ b/lib/features/mailbox_dashboard/presentation/model/dashboard_routes.dart @@ -3,5 +3,6 @@ enum DashboardRoutes { thread, emailDetailed, searchEmail, - waiting + waiting, + sendingQueue; } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/model/draggable_app_state.dart b/lib/features/mailbox_dashboard/presentation/model/draggable_app_state.dart new file mode 100644 index 0000000000..adab8f3def --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/model/draggable_app_state.dart @@ -0,0 +1,5 @@ + +enum DraggableAppState { + active, + inActive +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart b/lib/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart index d8d07e0117..eacb01da7d 100644 --- a/lib/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart +++ b/lib/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart @@ -39,11 +39,12 @@ enum AdvancedSearchFilterField { case AdvancedSearchFilterField.to: return AppLocalizations.of(context).nameOrEmailAddress; case AdvancedSearchFilterField.subject: + return AppLocalizations.of(context).enterASubject; case AdvancedSearchFilterField.hasKeyword: case AdvancedSearchFilterField.notKeyword: - return AppLocalizations.of(context).enterSearchTerm; + return AppLocalizations.of(context).enterSomeSuggestions; case AdvancedSearchFilterField.mailBox: - return AppLocalizations.of(context).allMails; + return AppLocalizations.of(context).allMailboxes; case AdvancedSearchFilterField.date: return AppLocalizations.of(context).allTime; case AdvancedSearchFilterField.hasAttachment: diff --git a/lib/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart b/lib/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart index 2add052b46..b54d7bcf60 100644 --- a/lib/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart +++ b/lib/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart @@ -37,10 +37,8 @@ enum EmailReceiveTimeType { } } - UTCDate? toUTCDate() { + UTCDate? toOldestUTCDate() { switch(this) { - case EmailReceiveTimeType.allTime: - return null; case EmailReceiveTimeType.last7Days: final today = DateTime.now(); final last7Days = today.subtract(const Duration(days: 7)); @@ -57,8 +55,49 @@ enum EmailReceiveTimeType { final today = DateTime.now(); final lastYear = DateTime(today.year - 1, today.month, today.day); return lastYear.toUTCDate(); - case EmailReceiveTimeType.customRange: + default: + return null; + } + } + + UTCDate? toLatestUTCDate() { + switch(this) { + case EmailReceiveTimeType.last7Days: + case EmailReceiveTimeType.last30Days: + case EmailReceiveTimeType.last6Months: + case EmailReceiveTimeType.lastYear: + return DateTime.now().toUTCDate(); + default: return null; } } + + UTCDate? getAfterDate(UTCDate? startDate) { + if (startDate != null) { + return startDate; + } else { + return toOldestUTCDate(); + } + } + + UTCDate? getBeforeDate(UTCDate? endDate, UTCDate? loadMoreDate) { + if (endDate != null) { + if (loadMoreDate != null && loadMoreDate.value.isBefore(endDate.value)) { + return loadMoreDate; + } else { + return endDate; + } + } else { + final latestDate = toLatestUTCDate(); + if (latestDate != null) { + if (loadMoreDate != null && loadMoreDate.value.isBefore(latestDate.value)) { + return loadMoreDate; + } else { + return latestDate; + } + } else { + return loadMoreDate; + } + } + } } diff --git a/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart b/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart index 7d05bc7813..f4a56cc2c2 100644 --- a/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart +++ b/lib/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart @@ -1,7 +1,10 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; import 'package:flutter/cupertino.dart'; +import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; enum QuickSearchFilter { @@ -9,22 +12,36 @@ enum QuickSearchFilter { last7Days, fromMe; - String getTitle(BuildContext context, {EmailReceiveTimeType? receiveTimeType}) { + String getName(BuildContext context) { switch(this) { case QuickSearchFilter.hasAttachment: return AppLocalizations.of(context).hasAttachment; case QuickSearchFilter.last7Days: - if (receiveTimeType != null) { - return receiveTimeType.getTitle(context); - } return AppLocalizations.of(context).last7Days; case QuickSearchFilter.fromMe: return AppLocalizations.of(context).fromMe; } } - String getIcon(ImagePaths imagePaths, {required bool quickSearchFilterSelected}) { - if (quickSearchFilterSelected) { + String getTitle( + BuildContext context, { + EmailReceiveTimeType? receiveTimeType, + DateTime? startDate, + DateTime? endDate, + }) { + switch(this) { + case QuickSearchFilter.hasAttachment: + return AppLocalizations.of(context).hasAttachment; + case QuickSearchFilter.last7Days: + return receiveTimeType?.getTitle(context, startDate: startDate, endDate: endDate) + ?? AppLocalizations.of(context).allTime; + case QuickSearchFilter.fromMe: + return AppLocalizations.of(context).fromMe; + } + } + + String getIcon(ImagePaths imagePaths, {required bool isFilterSelected}) { + if (isFilterSelected) { return imagePaths.icSelectedSB; } else { switch(this) { @@ -38,16 +55,16 @@ enum QuickSearchFilter { } } - Color getBackgroundColor({required bool quickSearchFilterSelected}) { - if (quickSearchFilterSelected) { + Color getBackgroundColor({required bool isFilterSelected}) { + if (isFilterSelected) { return AppColor.colorItemEmailSelectedDesktop; } else { return AppColor.colorButtonHeaderThread; } } - TextStyle getTextStyle({required bool quickSearchFilterSelected}) { - if (quickSearchFilterSelected) { + TextStyle getTextStyle({required bool isFilterSelected}) { + if (isFilterSelected) { return const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, @@ -59,4 +76,19 @@ enum QuickSearchFilter { color: AppColor.colorTextButtonHeaderThread); } } + + bool isApplied(List listFilter) => listFilter.contains(this); + + bool isSelected(SearchEmailFilter filter, UserProfile? userProfile) { + switch (this) { + case QuickSearchFilter.hasAttachment: + return filter.hasAttachment == true; + case QuickSearchFilter.last7Days: + return true; + case QuickSearchFilter.fromMe: + return userProfile != null && + filter.from.contains(userProfile.email) && + filter.from.length == 1; + } + } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart b/lib/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart index 38704a4abf..9789f8ad46 100644 --- a/lib/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart +++ b/lib/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart @@ -5,7 +5,9 @@ import 'package:jmap_dart_client/jmap/core/filter/filter_operator.dart'; import 'package:jmap_dart_client/jmap/core/filter/operator/logic_filter_operator.dart'; import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; -import 'package:model/model.dart'; +import 'package:model/extensions/email_filter_condition_extension.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; @@ -44,7 +46,7 @@ class SearchEmailFilter with EquatableMixin { emailReceiveTimeType ?? EmailReceiveTimeType.allTime; SearchEmailFilter copyWith({ - Set? from, + Option>? fromOption, Set? to, SearchQuery? text, Option? subjectOption, @@ -52,12 +54,12 @@ class SearchEmailFilter with EquatableMixin { PresentationMailbox? mailbox, EmailReceiveTimeType? emailReceiveTimeType, bool? hasAttachment, - UTCDate? before, - UTCDate? startDate, - UTCDate? endDate, + Option? beforeOption, + Option? startDateOption, + Option? endDateOption, }) { return SearchEmailFilter( - from: from ?? this.from, + from: _getOptionParam(fromOption, from), to: to ?? this.to, text: text ?? this.text, subject: _getOptionParam(subjectOption, subject), @@ -65,9 +67,9 @@ class SearchEmailFilter with EquatableMixin { mailbox: mailbox ?? this.mailbox, emailReceiveTimeType: emailReceiveTimeType ?? this.emailReceiveTimeType, hasAttachment: hasAttachment ?? this.hasAttachment, - before: before ?? this.before, - startDate: startDate ?? this.startDate, - endDate: endDate ?? this.endDate, + before: _getOptionParam(beforeOption, before), + startDate: _getOptionParam(startDateOption, startDate), + endDate: _getOptionParam(endDateOption, endDate), ); } @@ -84,17 +86,13 @@ class SearchEmailFilter with EquatableMixin { text: text?.value.trim().isNotEmpty == true ? text?.value : null, - inMailbox: mailbox == PresentationMailbox.unifiedMailbox - ? null - : mailbox?.id, - after: emailReceiveTimeType == EmailReceiveTimeType.customRange - ? startDate - : emailReceiveTimeType.toUTCDate(), + inMailbox: mailbox?.mailboxId, + after: emailReceiveTimeType.getAfterDate(startDate), hasAttachment: hasAttachment == false ? null : hasAttachment, - subject: subject, - before: emailReceiveTimeType == EmailReceiveTimeType.customRange - ? endDate - : before, + subject: subject?.trim().isNotEmpty == true + ? subject + : null, + before: emailReceiveTimeType.getBeforeDate(endDate, before) ); final listEmailCondition = { @@ -110,8 +108,8 @@ class SearchEmailFilter with EquatableMixin { from.map((e) => EmailFilterCondition(from: e)).toSet()), if (notKeyword.isNotEmpty) LogicFilterOperator( - Operator.AND, - notKeyword.map((e) => EmailFilterCondition(notKeyword: e)).toSet()), + Operator.NOT, + notKeyword.map((e) => EmailFilterCondition(text: e)).toSet()), if (moreFilterCondition != null && moreFilterCondition.hasCondition) moreFilterCondition }; @@ -135,39 +133,4 @@ class SearchEmailFilter with EquatableMixin { startDate, endDate ]; -} - -extension SearchEmailFilterExtension on SearchEmailFilter { - - SearchEmailFilter clearBeforeDate() { - return SearchEmailFilter( - from: from, - to: to, - text: text, - subject: subject, - notKeyword: notKeyword, - mailbox: mailbox, - emailReceiveTimeType: emailReceiveTimeType, - hasAttachment: hasAttachment, - before: null, - startDate: startDate, - endDate: endDate, - ); - } - - SearchEmailFilter withDateRange({UTCDate? startDate, UTCDate? endDate}) { - return SearchEmailFilter( - from: from, - to: to, - text: text, - subject: subject, - notKeyword: notKeyword, - mailbox: mailbox, - emailReceiveTimeType: emailReceiveTimeType, - hasAttachment: hasAttachment, - before: before, - startDate: startDate, - endDate: endDate, - ); - } } \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/styles/advanced_search_input_form_style.dart b/lib/features/mailbox_dashboard/presentation/styles/advanced_search_input_form_style.dart new file mode 100644 index 0000000000..ea6833647d --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/styles/advanced_search_input_form_style.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +class AdvancedSearchInputFormStyle { + static const TextStyle inputTextStyle = TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w400, + ); +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/styles/autocomplete_suggestion_item_style.dart b/lib/features/mailbox_dashboard/presentation/styles/autocomplete_suggestion_item_style.dart new file mode 100644 index 0000000000..fa9bbe43b5 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/styles/autocomplete_suggestion_item_style.dart @@ -0,0 +1,20 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AutocompleteSuggestionItemStyle { + static const double space = 10; + + static const EdgeInsetsGeometry padding = EdgeInsets.symmetric(vertical: 10, horizontal: 12); + + static const TextStyle displayNameTextStyle = TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.normal + ); + static const TextStyle emailAddressNameTextStyle = TextStyle( + color: AppColor.colorHintSearchBar, + fontSize: 13, + fontWeight: FontWeight.normal + ); +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/styles/autocomplete_tag_item_style.dart b/lib/features/mailbox_dashboard/presentation/styles/autocomplete_tag_item_style.dart new file mode 100644 index 0000000000..c9a3df247b --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/styles/autocomplete_tag_item_style.dart @@ -0,0 +1,19 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AutocompleteTagItemStyle { + static const double radius = 10; + static const double space = 4; + static const double deleteIconSize = 24; + + static Color get backgroundColor => AppColor.colorBackgroundTagFilter.withOpacity(0.08); + + static const EdgeInsetsGeometry margin = EdgeInsetsDirectional.only(end: 8); + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.only(start: 8, end: 4, top: 4, bottom: 4); + + static const TextStyle labelTextStyle = TextStyle( + color: Colors.black, + fontSize: 17, + fontWeight: FontWeight.normal + ); +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/styles/avatar_suggestion_item_style.dart b/lib/features/mailbox_dashboard/presentation/styles/avatar_suggestion_item_style.dart new file mode 100644 index 0000000000..c3f946000e --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/styles/avatar_suggestion_item_style.dart @@ -0,0 +1,17 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AvatarSuggestionItemStyle { + static const double iconSize = 40; + static const double iconBorderSize = 1.0; + + static const Color iconColor = AppColor.avatarColor; + static const Color iconBorderColor = AppColor.colorShadowBgContentEmail; + + static const TextStyle labelTextStyle = TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w600 + ); +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/styles/avatar_tag_item_style.dart b/lib/features/mailbox_dashboard/presentation/styles/avatar_tag_item_style.dart new file mode 100644 index 0000000000..a1430d6557 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/styles/avatar_tag_item_style.dart @@ -0,0 +1,15 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class AvatarTagItemStyle { + static const double iconSize = 24; + static const double iconBorderSize = 0.21; + + static const Color iconBorderColor = AppColor.colorShadowBgContentEmail; + + static const TextStyle labelTextStyle = TextStyle( + color: Colors.white, + fontSize: 8.57, + fontWeight: FontWeight.w600 + ); +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/styles/text_field_autocomplete_email_address_style.dart b/lib/features/mailbox_dashboard/presentation/styles/text_field_autocomplete_email_address_style.dart new file mode 100644 index 0000000000..75290d2bcd --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/styles/text_field_autocomplete_email_address_style.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +class TextFieldAutocompleteEmailAddressStyle { + static const TextStyle inputTextStyle = TextStyle( + color: Colors.black, + fontSize: 16, + fontWeight: FontWeight.w400, + ); +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_bottom_sheet.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_bottom_sheet.dart index 614c624a19..cd7c5d7b23 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_bottom_sheet.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_bottom_sheet.dart @@ -2,11 +2,11 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_input_form.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; Future showAdvancedSearchFilterBottomSheet(BuildContext context) async { - final ImagePaths _imagePaths = Get.find(); + final ImagePaths imagePaths = Get.find(); await FullScreenActionSheetBuilder( context: context, @@ -19,8 +19,8 @@ Future showAdvancedSearchFilterBottomSheet(BuildContext context) async { cancelWidget: Padding( padding: const EdgeInsets.only(right: 16), child: SvgPicture.asset( - _imagePaths.icCloseAdvancedSearch, - color: AppColor.colorHintSearchBar, + imagePaths.icCircleClose, + colorFilter: AppColor.colorHintSearchBar.asFilter(), width: 24, height: 24, ), diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart index 4bca3ab1e1..3626a3504f 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart @@ -1,4 +1,7 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/presentation/views/checkbox/labeled_checkbox.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; @@ -18,47 +21,29 @@ class AdvancedSearchFilterFormBottomView extends GetWidget(); + final responsiveUtils = Get.find(); return Padding( - padding: EdgeInsets.only( - top: _isMobileAndLandscapeTablet(context, _responsiveUtils) ? 8 : 20), + padding: EdgeInsets.only(top: !responsiveUtils.isWebDesktop(context) ? 8 : 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (_isMobileAndLandscapeTablet(context, _responsiveUtils)) - ...[ - _buildCheckboxHasAttachment( - context, - currentFocusNode: focusManager?.attachmentCheckboxFocusNode, - nextFocusNode: focusManager?.searchButtonFocusNode), - const SizedBox(height: 24) - ], - Row( - mainAxisAlignment: _isMobileAndLandscapeTablet(context, _responsiveUtils) - ? MainAxisAlignment.spaceEvenly - : MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (!_isMobileAndLandscapeTablet(context, _responsiveUtils)) - Expanded(child: _buildCheckboxHasAttachment( - context, - currentFocusNode: focusManager?.attachmentCheckboxFocusNode, - nextFocusNode: focusManager?.searchButtonFocusNode)), - ..._buildListButton(context, _responsiveUtils), - ], + Transform( + transform: Matrix4.translationValues(-8.0, 0.0, 0.0), + child: _buildCheckboxHasAttachment( + context, + currentFocusNode: focusManager?.attachmentCheckboxFocusNode, + nextFocusNode: focusManager?.searchButtonFocusNode), ), + _buildListButton(context, responsiveUtils), ], ), ); } - List _buildListButton( - BuildContext context, - ResponsiveUtils responsiveUtils - ) { - if (_isMobileAndLandscapeTablet(context, responsiveUtils)) { - return [ + Widget _buildListButton(BuildContext context, ResponsiveUtils responsiveUtils) { + if (!responsiveUtils.isWebDesktop(context)) { + return Row(children: [ Expanded( child: _buildButton( onAction: () { @@ -88,15 +73,16 @@ class AdvancedSearchFilterFormBottomView extends GetWidget FocusManager.instance.primaryFocus?.unfocus(), - child: Padding( - padding: const EdgeInsets.only(top: 4, bottom: 16), - child: Container( - constraints: BoxConstraints( - maxHeight: _getHeightOverlay(context, responsiveUtils), - ), - width: maxWidth ?? 660, - padding: const EdgeInsets.all(32), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(16), - boxShadow: const [ - BoxShadow( - color: AppColor.colorShadowComposer, - blurRadius: 32, - offset: Offset.zero), - BoxShadow( - color: AppColor.colorDropShadow, - blurRadius: 4, - offset: Offset.zero), - ]), - child: SingleChildScrollView( - child: AdvancedSearchInputForm(), - ), + child: Container( + constraints: BoxConstraints( + maxHeight: _getHeightOverlay(context, responsiveUtils), + ), + margin: const EdgeInsetsDirectional.only(top: 4, bottom: 16, end: 22), + padding: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: const [ + BoxShadow( + color: AppColor.colorShadowComposer, + blurRadius: 32, + offset: Offset.zero), + BoxShadow( + color: AppColor.colorDropShadow, + blurRadius: 4, + offset: Offset.zero), + ] + ), + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24), + child: AdvancedSearchInputForm(), ), ), ), diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_input_form.dart similarity index 66% rename from lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form.dart rename to lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_input_form.dart index 40b62b46fc..8cc6d4f0b8 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_input_form.dart @@ -4,13 +4,16 @@ import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/mixin/popup_context_menu_action_mixin.dart'; +import 'package:tmail_ui_user/features/base/widget/popup_item_no_icon_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/advanced_search_input_form_style.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/advanced_search_filter_form_bottom_view.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/drop_down_button_filter_widget.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_auto_complete_email_adress.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/date_drop_down_button.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_autocomplete_email_adress.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class AdvancedSearchInputForm extends GetWidget with PopupContextMenuActionMixin { @@ -23,46 +26,47 @@ class AdvancedSearchInputForm extends GetWidget @override Widget build(BuildContext context) { - return Column( - children: [ - _buildSuggestionFilterField( - listTagSelected: controller.searchEmailFilter.from, - context: context, - advancedSearchFilterField: AdvancedSearchFilterField.form, - listTagInitial: controller.searchEmailFilter.from, - currentFocusNode: controller.focusManager.fromFieldFocusNode, - nextFocusNode: controller.focusManager.toFieldFocusNode - ), - _buildSuggestionFilterField( - listTagSelected: controller.searchEmailFilter.to, - context: context, - advancedSearchFilterField: AdvancedSearchFilterField.to, - listTagInitial: controller.searchEmailFilter.to, - currentFocusNode: controller.focusManager.toFieldFocusNode, - nextFocusNode: controller.focusManager.subjectFieldFocusNode - ), - _buildFilterField( - textEditingController: controller.subjectFilterInputController, - context: context, - advancedSearchFilterField: AdvancedSearchFilterField.subject, - currentFocusNode: controller.focusManager.subjectFieldFocusNode, - nextFocusNode: controller.focusManager.hasKeywordFieldFocusNode - ), - _buildFilterField( - textEditingController: controller.hasKeyWordFilterInputController, - context: context, - advancedSearchFilterField: AdvancedSearchFilterField.hasKeyword, - currentFocusNode: controller.focusManager.hasKeywordFieldFocusNode, - nextFocusNode: controller.focusManager.notKeywordFieldFocusNode - ), - _buildFilterField( - textEditingController: controller.notKeyWordFilterInputController, - context: context, - advancedSearchFilterField: AdvancedSearchFilterField.notKeyword, - currentFocusNode: controller.focusManager.notKeywordFieldFocusNode, - nextFocusNode: controller.focusManager.mailboxFieldFocusNode, - ), - _buildFilterField( + return FocusTraversalGroup( + child: Column( + children: [ + _buildSuggestionFilterField( + listTagSelected: controller.searchEmailFilter.from, + context: context, + advancedSearchFilterField: AdvancedSearchFilterField.form, + listTagInitial: controller.searchEmailFilter.from, + currentFocusNode: controller.focusManager.fromFieldFocusNode, + nextFocusNode: controller.focusManager.toFieldFocusNode + ), + _buildSuggestionFilterField( + listTagSelected: controller.searchEmailFilter.to, + context: context, + advancedSearchFilterField: AdvancedSearchFilterField.to, + listTagInitial: controller.searchEmailFilter.to, + currentFocusNode: controller.focusManager.toFieldFocusNode, + nextFocusNode: controller.focusManager.subjectFieldFocusNode + ), + _buildFilterField( + textEditingController: controller.subjectFilterInputController, + context: context, + advancedSearchFilterField: AdvancedSearchFilterField.subject, + currentFocusNode: controller.focusManager.subjectFieldFocusNode, + nextFocusNode: controller.focusManager.hasKeywordFieldFocusNode + ), + _buildFilterField( + textEditingController: controller.hasKeyWordFilterInputController, + context: context, + advancedSearchFilterField: AdvancedSearchFilterField.hasKeyword, + currentFocusNode: controller.focusManager.hasKeywordFieldFocusNode, + nextFocusNode: controller.focusManager.notKeywordFieldFocusNode + ), + _buildFilterField( + textEditingController: controller.notKeyWordFilterInputController, + context: context, + advancedSearchFilterField: AdvancedSearchFilterField.notKeyword, + currentFocusNode: controller.focusManager.notKeywordFieldFocusNode, + nextFocusNode: controller.focusManager.mailboxFieldFocusNode, + ), + _buildFilterField( textEditingController: controller.mailBoxFilterInputController, context: context, advancedSearchFilterField: AdvancedSearchFilterField.mailBox, @@ -70,84 +74,55 @@ class AdvancedSearchInputForm extends GetWidget currentFocusNode: controller.focusManager.mailboxFieldFocusNode, nextFocusNode: controller.focusManager.attachmentCheckboxFocusNode, mouseCursor: SystemMouseCursors.click, - onTap: () => controller.selectedMailBox(context)), - Row(children: [ - Expanded(child: _buildFilterField( - textEditingController: controller.dateFilterInputController, - context: context, - advancedSearchFilterField: AdvancedSearchFilterField.date, - isSelectFormList: true, - onTap: () { - openContextMenuAction( - context, - _buildEmailReceiveTimeTypeActionTiles(context), - ); - }, - )), - const SizedBox(width: 10), - buildIconWeb( - icon: SvgPicture.asset( - _imagePaths.icCalendarSB, - width: 24, - height: 24, - fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).selectDate, - iconPadding: EdgeInsets.zero, - onTap: () => controller.selectDateRange(context)), - ]), - AdvancedSearchFilterFormBottomView(focusManager: controller.focusManager) - ], + onTap: () => controller.selectedMailBox(context) + ), + Row(children: [ + Expanded(child: _buildFilterField( + context: context, + advancedSearchFilterField: AdvancedSearchFilterField.date, + isSelectFormList: true, + onTap: () { + openContextMenuAction( + context, + _buildEmailReceiveTimeTypeActionTiles(context), + ); + }, + )), + const SizedBox(width: 10), + buildIconWeb( + icon: SvgPicture.asset( + _imagePaths.icCalendarSB, + width: 24, + height: 24, + fit: BoxFit.fill), + tooltip: AppLocalizations.of(context).selectDate, + iconPadding: EdgeInsets.zero, + onTap: () => controller.selectDateRange(context)), + ]), + AdvancedSearchFilterFormBottomView(focusManager: controller.focusManager) + ], + ), ); } List _buildEmailReceiveTimeTypeActionTiles(BuildContext context) { return EmailReceiveTimeType.values - .map( - (e) => Material( - child: PopupMenuItem( - child: Row(children: [ - const SizedBox(width: 12), - Expanded( - child: Text( - e.getTitle( - context, - startDate: controller.startDate, - endDate: controller.endDate), - style: const TextStyle( - fontSize: 15, - color: Colors.black, - fontWeight: FontWeight.w500))), - if (e == controller.dateFilterSelectedFormAdvancedSearch.value) - ...[ - const SizedBox(width: 12), - SvgPicture.asset( - _imagePaths.icFilterSelected, - width: 16, - height: 16, - fit: BoxFit.fill, - ), - ] - ]), - onTap: () { - if (e != EmailReceiveTimeType.customRange) { - controller.clearDateRangeOfFilter(); - } - controller.dateFilterSelectedFormAdvancedSearch.value = e; - controller.dateFilterInputController.text = e.getTitle( - context, - startDate: controller.startDate, - endDate: controller.endDate); - }, - ), - ), - ) - .toList(); + .map((receiveTime) => PopupMenuItem( + padding: EdgeInsets.zero, + child: PopupItemNoIconWidget( + receiveTime.getTitle(context), + svgIconSelected: _imagePaths.icFilterSelected, + maxWidth: 320, + isSelected: controller.dateFilterSelectedFormAdvancedSearch.value == receiveTime, + onCallbackAction: () => controller.updateReceiveDateSearchFilter(context, receiveTime), + ))) + .toList(); } Widget _buildFilterField({ required BuildContext context, required AdvancedSearchFilterField advancedSearchFilterField, - required TextEditingController textEditingController, + TextEditingController? textEditingController, VoidCallback? onTap, bool isSelectFormList = false, MouseCursor? mouseCursor, @@ -180,7 +155,13 @@ class AdvancedSearchInputForm extends GetWidget ) else if (_responsiveUtils.landscapeTabletSupported(context)) if (advancedSearchFilterField == AdvancedSearchFilterField.date) - const DateDropDownButton() + Obx(() => DateDropDownButton( + _imagePaths, + startDate: controller.startDate.value, + endDate: controller.endDate.value, + receiveTimeSelected: controller.dateFilterSelectedFormAdvancedSearch.value, + onReceiveTimeSelected: (receiveTime) => controller.updateReceiveDateSearchFilter(context, receiveTime), + )) else _buildTextField( isSelectFormList: isSelectFormList, @@ -222,7 +203,7 @@ class AdvancedSearchInputForm extends GetWidget Widget _buildTextFieldFilterForWeb({ required BuildContext context, required AdvancedSearchFilterField advancedSearchFilterField, - required TextEditingController textEditingController, + TextEditingController? textEditingController, VoidCallback? onTap, bool isSelectFormList = false, MouseCursor? mouseCursor, @@ -231,7 +212,13 @@ class AdvancedSearchInputForm extends GetWidget }) { switch (advancedSearchFilterField) { case AdvancedSearchFilterField.date: - return const DateDropDownButton(); + return Obx(() => DateDropDownButton( + _imagePaths, + startDate: controller.startDate.value, + endDate: controller.endDate.value, + receiveTimeSelected: controller.dateFilterSelectedFormAdvancedSearch.value, + onReceiveTimeSelected: (receiveTime) => controller.updateReceiveDateSearchFilter(context, receiveTime), + )); default: return _buildTextField( isSelectFormList: isSelectFormList, @@ -268,7 +255,7 @@ class AdvancedSearchInputForm extends GetWidget ), const Padding(padding: EdgeInsets.all(4)), _responsiveUtils.isMobile(context) || _responsiveUtils.landscapeTabletSupported(context) - ? TextFieldAutoCompleteEmailAddress( + ? TextFieldAutocompleteEmailAddress( optionsBuilder: controller.getAutoCompleteSuggestion, advancedSearchFilterField: advancedSearchFilterField, initialTags: listTagInitial, @@ -304,7 +291,7 @@ class AdvancedSearchInputForm extends GetWidget }, ) : Expanded( - child: TextFieldAutoCompleteEmailAddress( + child: TextFieldAutocompleteEmailAddress( optionsBuilder: controller.getAutoCompleteSuggestion, advancedSearchFilterField: advancedSearchFilterField, initialTags: listTagInitial, @@ -357,7 +344,7 @@ class AdvancedSearchInputForm extends GetWidget Widget _buildTextField({ required BuildContext context, required AdvancedSearchFilterField advancedSearchFilterField, - required TextEditingController textEditingController, + TextEditingController? textEditingController, VoidCallback? onTap, bool isSelectFormList = false, MouseCursor? mouseCursor, @@ -372,32 +359,47 @@ class AdvancedSearchInputForm extends GetWidget nextFocusNode?.requestFocus(); } }, - child: TextField( + child: TextFieldBuilder( controller: textEditingController, readOnly: isSelectFormList, mouseCursor: mouseCursor, - textInputAction: TextInputAction.next, + maxLines: 1, + textInputAction: isSelectFormList ? TextInputAction.done : TextInputAction.next, + textStyle: AdvancedSearchInputFormStyle.inputTextStyle, onTap: onTap, + onTextSubmitted: (value) { + if (isSelectFormList) { + onTap?.call(); + } else { + FocusScope.of(context).unfocus(); + controller.applyAdvancedSearchFilter(context); + popBack(); + } + }, decoration: InputDecoration( filled: true, - fillColor: AppColor.loginTextFieldBackgroundColor, + fillColor: isSelectFormList ? AppColor.colorItemSelected : Colors.white, contentPadding: const EdgeInsets.only( right: 8, left: 12, ), - enabledBorder: const OutlineInputBorder( - borderRadius: BorderRadius.all( + enabledBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all( Radius.circular(10), ), borderSide: BorderSide( - width: 0.5, + width: isSelectFormList ? 0.5 : 1, color: AppColor.colorInputBorderCreateMailbox, ), ), - border: const OutlineInputBorder( - borderRadius: BorderRadius.all( + border: OutlineInputBorder( + borderRadius: const BorderRadius.all( Radius.circular(10), ), + borderSide: BorderSide( + width: isSelectFormList ? 0.5 : 1, + color: AppColor.colorInputBorderCreateMailbox, + ), ), hintText: advancedSearchFilterField.getHintText(context), hintStyle: TextStyle( diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_suggestion_item_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_suggestion_item_widget.dart new file mode 100644 index 0000000000..263dcde2cd --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_suggestion_item_widget.dart @@ -0,0 +1,64 @@ +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/autocomplete_suggestion_item_style.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_suggestion_item_widget.dart'; + +typedef OnSelectSuggestionItemCallback = Function(String emailAddress); + +class AutocompleteSuggestionItemWidget extends StatelessWidget { + + final EmailAddress emailAddress; + final OnSelectSuggestionItemCallback onSelectCallback; + + const AutocompleteSuggestionItemWidget({ + super.key, + required this.emailAddress, + required this.onSelectCallback, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onSelectCallback(emailAddress.emailAddress), + child: Container( + padding: AutocompleteSuggestionItemStyle.padding, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + AvatarSuggestionItemWidget(emailAddress: emailAddress), + const SizedBox(width: AutocompleteSuggestionItemStyle.space), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (emailAddress.displayName.isNotEmpty) + Text( + emailAddress.displayName, + maxLines: 1, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + style: AutocompleteSuggestionItemStyle.displayNameTextStyle + ), + if (emailAddress.emailAddress.isNotEmpty) + Text( + emailAddress.emailAddress, + maxLines: 1, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + style: AutocompleteSuggestionItemStyle.emailAddressNameTextStyle + ) + ] + ) + ), + ] + ), + ), + ), + ); + } +} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_tag_item_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_tag_item_widget.dart new file mode 100644 index 0000000000..18d5800876 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_tag_item_widget.dart @@ -0,0 +1,59 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/autocomplete_tag_item_style.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_tag_item_widget.dart'; + +typedef OnDeleteTagItemCallback = Function(String tagName); + +class AutocompleteTagItemWidget extends StatelessWidget { + + final String tagName; + final OnDeleteTagItemCallback onDeleteCallback; + + const AutocompleteTagItemWidget({ + super.key, + required this.tagName, + required this.onDeleteCallback, + }); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + return Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all( + Radius.circular(AutocompleteTagItemStyle.radius), + ), + color: AutocompleteTagItemStyle.backgroundColor, + ), + margin: AutocompleteTagItemStyle.margin, + padding: AutocompleteTagItemStyle.padding, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + AvatarTagItemWidget(tagName: tagName), + const SizedBox(width: AutocompleteTagItemStyle.space), + Text( + tagName, + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: AutocompleteTagItemStyle.labelTextStyle + ), + const SizedBox(width: AutocompleteTagItemStyle.space), + TMailButtonWidget.fromIcon( + icon: imagePaths.icClose, + iconSize: AutocompleteTagItemStyle.deleteIconSize, + backgroundColor: Colors.transparent, + padding: EdgeInsets.zero, + onTapActionCallback: () => onDeleteCallback(tagName), + ) + ], + ), + ); + } +} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_suggestion_item_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_suggestion_item_widget.dart new file mode 100644 index 0000000000..b577f74d06 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_suggestion_item_widget.dart @@ -0,0 +1,35 @@ +import 'package:core/presentation/extensions/string_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/extensions/email_address_extension.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/avatar_suggestion_item_style.dart'; + +class AvatarSuggestionItemWidget extends StatelessWidget { + + final EmailAddress emailAddress; + + const AvatarSuggestionItemWidget({super.key, required this.emailAddress}); + + @override + Widget build(BuildContext context) { + return Container( + width: AvatarSuggestionItemStyle.iconSize, + height: AvatarSuggestionItemStyle.iconSize, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AvatarSuggestionItemStyle.iconColor, + border: Border.all( + color: AvatarSuggestionItemStyle.iconBorderColor, + width: AvatarSuggestionItemStyle.iconBorderSize + ) + ), + child: Text( + emailAddress.asString().isNotEmpty + ? emailAddress.asString().firstLetterToUpperCase + : '', + style: AvatarSuggestionItemStyle.labelTextStyle + ) + ); + } +} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_tag_item_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_tag_item_widget.dart new file mode 100644 index 0000000000..e00347971e --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/avatar_tag_item_widget.dart @@ -0,0 +1,47 @@ +import 'package:collection/collection.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/avatar_tag_item_style.dart'; + +class AvatarTagItemWidget extends StatelessWidget { + final String tagName; + + const AvatarTagItemWidget({super.key, required this.tagName}); + + @override + Widget build(BuildContext context) { + return Container( + width: AvatarTagItemStyle.iconSize, + height: AvatarTagItemStyle.iconSize, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: AppColor.mapGradientColor[_generateIndex(tagName)], + ), + border: Border.all( + color: AvatarTagItemStyle.iconBorderColor, + width: AvatarTagItemStyle.iconBorderSize + ) + ), + child: Text( + tagName.isNotEmpty ? tagName[0].toUpperCase() : '', + style: AvatarTagItemStyle.labelTextStyle + ) + ); + } + + int _generateIndex(String tag) { + if (tag.isNotEmpty) { + final codeUnits = tag.codeUnits; + if (codeUnits.isNotEmpty) { + final sumCodeUnits = codeUnits.sum; + final index = sumCodeUnits % AppColor.mapGradientColor.length; + return index; + } + } + return 0; + } +} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/date_drop_down_button.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/date_drop_down_button.dart new file mode 100644 index 0000000000..1bf2451931 --- /dev/null +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/date_drop_down_button.dart @@ -0,0 +1,137 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:dropdown_button2/dropdown_button2.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; + +class DateDropDownButton extends StatelessWidget { + + final ImagePaths imagePaths; + final DateTime? startDate; + final DateTime? endDate; + final EmailReceiveTimeType? receiveTimeSelected; + final Function(EmailReceiveTimeType)? onReceiveTimeSelected; + + const DateDropDownButton( + this.imagePaths, { + Key? key, + this.startDate, + this.endDate, + this.receiveTimeSelected, + this.onReceiveTimeSelected, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DropdownButtonHideUnderline( + child: PointerInterceptor( + child: DropdownButton2( + isExpanded: true, + items: EmailReceiveTimeType.values + .map((item) => _buildItemMenu(context, item)) + .toList(), + value: receiveTimeSelected, + customButton: Container( + height: 44, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: AppColor.colorInputBorderCreateMailbox, + width: 0.5, + ), + color: AppColor.colorInputBackgroundCreateMailbox + ), + padding: const EdgeInsets.only(left: 12, right: 10), + child: Row(children: [ + Expanded(child: Text( + receiveTimeSelected?.getTitle(context, startDate: startDate, endDate: endDate) ?? '', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black + ), + maxLines: 1, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + )), + SvgPicture.asset(imagePaths.icDropDown) + ]), + ), + onChanged: (value) { + if (value != null) { + onReceiveTimeSelected?.call(value); + } + }, + buttonStyleData: ButtonStyleData( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: AppColor.colorInputBorderCreateMailbox, + width: 1), + color: AppColor.colorInputBackgroundCreateMailbox) + ), + dropdownStyleData: DropdownStyleData( + maxHeight: 200, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.white), + elevation: 4, + offset: const Offset(0.0, -8.0), + scrollbarTheme: ScrollbarThemeData( + radius: const Radius.circular(40), + thickness: MaterialStateProperty.all(6), + thumbVisibility: MaterialStateProperty.all(true), + ), + ), + iconStyleData: IconStyleData( + icon: SvgPicture.asset(imagePaths.icDropDown), + ), + menuItemStyleData: const MenuItemStyleData( + height: 44, + padding: EdgeInsets.symmetric(horizontal: 12), + ) + ), + ), + ); + } + + DropdownMenuItem _buildItemMenu( + BuildContext context, + EmailReceiveTimeType receiveTime + ) { + return DropdownMenuItem( + value: receiveTime, + child: PointerInterceptor( + child: Container( + color: Colors.transparent, + height: 44, + child: Row(children: [ + Expanded(child: Text( + receiveTime.getTitle(context), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black + ), + maxLines: 1, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + )), + if (receiveTime == receiveTimeSelected) + SvgPicture.asset( + imagePaths.icChecked, + width: 20, + height: 20, + fit: BoxFit.fill + ) + ]), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/drop_down_button_filter_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/drop_down_button_filter_widget.dart deleted file mode 100644 index 8298c917ea..0000000000 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/drop_down_button_filter_widget.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'package:core/core.dart'; -import 'package:dropdown_button2/dropdown_button2.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; - -class DateDropDownButton extends GetWidget { - const DateDropDownButton({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return Obx( - () => _buildDropDownButton( - items: EmailReceiveTimeType.values, - itemSelected: controller.dateFilterSelectedFormAdvancedSearch.value, - context: context, - startDate: controller.startDate, - endDate: controller.endDate, - onChanged: (item) { - if (item != EmailReceiveTimeType.customRange) { - controller.clearDateRangeOfFilter(); - } - controller.dateFilterSelectedFormAdvancedSearch.value = item!; - controller.dateFilterInputController.text = item.getTitle( - context, - startDate: controller.startDate, - endDate: controller.endDate); - }, - ), - ); - } -} - -Widget _buildDropDownButton({ - required List items, - required T itemSelected, - required BuildContext context, - required Function(T?)? onChanged, - DateTime? startDate, - DateTime? endDate -}) { - final ImagePaths _imagePaths = Get.find(); - - return DropdownButtonHideUnderline( - child: DropdownButton2( - isExpanded: true, - items: items - .map((item) => DropdownMenuItem( - value: item, - child: Text( - StringConvert.writeNullToEmpty(_getTextItemDropdown( - item: item, - context: context, - startDate: startDate, - endDate: endDate, - )), - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - color: Colors.black), - maxLines: 1, - overflow: BuildUtils.isWeb ? null : TextOverflow.ellipsis, - ), - )) - .toList(), - value: itemSelected, - onChanged: onChanged, - icon: SvgPicture.asset(_imagePaths.icDropDown), - buttonPadding: const EdgeInsets.symmetric(horizontal: 12), - buttonDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: AppColor.colorInputBorderCreateMailbox, - width: 0.5, - ), - color: AppColor.colorInputBackgroundCreateMailbox, - ), - itemHeight: 44, - buttonHeight: 44, - selectedItemHighlightColor: AppColor.primaryColor.withOpacity(0.12), - itemPadding: const EdgeInsets.symmetric(horizontal: 12), - dropdownMaxHeight: 200, - dropdownDecoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.white, - ), - dropdownElevation: 4, - scrollbarRadius: const Radius.circular(40), - scrollbarThickness: 6, - ), - ); -} - -String? _getTextItemDropdown({required T item, - required BuildContext context, - DateTime? startDate, - DateTime? endDate -}) { - if (item is EmailReceiveTimeType) { - return item.getTitle(context, startDate: startDate, endDate: endDate); - } - - if (item is Mailbox) { - return item.name?.name; - } - - return null; -} diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/icon_open_advanced_search_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/icon_open_advanced_search_widget.dart index 2035bf0476..3b08d0fc20 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/icon_open_advanced_search_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/icon_open_advanced_search_widget.dart @@ -1,11 +1,13 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/advanced_filter_controller.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; class IconOpenAdvancedSearchWidget extends StatelessWidget { IconOpenAdvancedSearchWidget( @@ -14,7 +16,7 @@ class IconOpenAdvancedSearchWidget extends StatelessWidget { }) : super(key: key); final _imagePaths = Get.find(); - final SearchController searchController = Get.find(); + final search.SearchController searchController = Get.find(); final AdvancedFilterController advancedFilterController = Get.find(); final BuildContext _parentContext; @@ -22,21 +24,26 @@ class IconOpenAdvancedSearchWidget extends StatelessWidget { Widget build(BuildContext context) { return Obx( () => Padding( - padding: const EdgeInsets.only(right: 8), + padding: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) ? 0 : 8, + left: AppUtils.isDirectionRTL(context) ? 8 : 0, + ), child: buildIconWeb( splashRadius: 15, minSize: 40, - iconPadding: const EdgeInsets.only(right: 2), + iconPadding: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) ? 0 : 2, + left: AppUtils.isDirectionRTL(context) ? 2 : 0, + ), icon: SvgPicture.asset(_imagePaths.icFilterAdvanced, - color: searchController.isAdvancedSearchViewOpen.isTrue || searchController.advancedSearchIsActivated.isTrue - ? AppColor.colorFilterMessageEnabled - : AppColor.colorFilterMessageDisabled, + colorFilter: searchController.isAdvancedSearchViewOpen.isTrue || searchController.advancedSearchIsActivated.isTrue + ? AppColor.colorFilterMessageEnabled.asFilter() + : AppColor.colorFilterMessageDisabled.asFilter(), width: 16, height: 16), onTap: () { - if(searchController.isAdvancedSearchViewOpen.isFalse && searchController.advancedSearchIsActivated.isFalse){ - advancedFilterController.initSearchFilterField(context); - } + log('IconOpenAdvancedSearchWidget::build(): clicked'); + advancedFilterController.initSearchFilterField(context); searchController.showAdvancedFilterView(_parentContext); }), ), diff --git a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_auto_complete_email_adress.dart b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_autocomplete_email_adress.dart similarity index 50% rename from lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_auto_complete_email_adress.dart rename to lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_autocomplete_email_adress.dart index a5e3def8ed..891cd897e1 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_auto_complete_email_adress.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/advanced_search/text_field_autocomplete_email_adress.dart @@ -1,19 +1,22 @@ -import 'package:collection/collection.dart'; -import 'package:core/core.dart'; -import 'package:flutter/foundation.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/text/text_field_builder.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:textfield_tags/textfield_tags.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/advanced_search_filter.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/custom_tf_tag_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/styles/text_field_autocomplete_email_address_style.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_suggestion_item_widget.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/advanced_search/autocomplete_tag_item_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -class TextFieldAutoCompleteEmailAddress extends StatefulWidget { - const TextFieldAutoCompleteEmailAddress({ +class TextFieldAutocompleteEmailAddress extends StatefulWidget { + const TextFieldAutocompleteEmailAddress({ Key? key, required this.advancedSearchFilterField, required this.initialTags, @@ -34,14 +37,13 @@ class TextFieldAutoCompleteEmailAddress extends StatefulWidget { final FocusNode? nextFocusNode; @override - State createState() => - _TextFieldAutoCompleteEmailAddressState(); + State createState() => + _TextFieldAutocompleteEmailAddressState(); } -class _TextFieldAutoCompleteEmailAddressState - extends State { +class _TextFieldAutocompleteEmailAddressState + extends State { final double _distanceToField = 380; - final ImagePaths _imagePaths = Get.find(); final _responsiveUtils = Get.find(); late CustomController _controller; @@ -66,8 +68,8 @@ class _TextFieldAutoCompleteEmailAddressState optionsViewBuilder: (context, onSelected, listEmailAddress) { return Container( margin: const EdgeInsets.only( - top: BuildUtils.isWeb ? 5 : 8, - bottom: 16), + top: PlatformInfo.isWeb ? 5 : 8, + bottom: 16), height: _getHeightSuggestionBox(listEmailAddress.length, 65), width: maxWidthSuggestionBox, alignment: Alignment.topLeft, @@ -88,9 +90,9 @@ class _TextFieldAutoCompleteEmailAddressState itemCount: listEmailAddress.length, itemBuilder: (BuildContext context, int index) { final emailAddress = listEmailAddress.elementAt(index); - return InkWell( - onTap: () => onSelected(emailAddress), - child: _buildSuggestionItem(context, emailAddress), + return AutocompleteSuggestionItemWidget( + emailAddress: emailAddress, + onSelectCallback: _controller.onSubmitted, ); }, ), @@ -106,7 +108,7 @@ class _TextFieldAutoCompleteEmailAddressState return widget.optionsBuilder.call(textEditingValue.text.toLowerCase()); }, onSelected: (EmailAddress selectedTag) { - _controller.addTag = selectedTag.asString(); + _controller.addTag = selectedTag.emailAddress; }, fieldViewBuilder: (context, ttec, tfn, onFieldSubmitted) { return TextFieldTags( @@ -114,7 +116,7 @@ class _TextFieldAutoCompleteEmailAddressState textEditingController: ttec, focusNode: tfn, textfieldTagsController: _controller, - textSeparators: const [' ', ','], + textSeparators: const ['\n', ','], letterCase: LetterCase.normal, validator: (String tag) { if (_controller.getTags!.contains(tag)) { @@ -127,20 +129,20 @@ class _TextFieldAutoCompleteEmailAddressState return RawKeyboardListener( focusNode: widget.currentFocusNode ?? FocusNode(), onKey: (event) { - log('_TextFieldAutoCompleteEmailAddressState::inputfieldBuilder(): Event runtimeType is ${event.runtimeType}'); if (event is RawKeyDownEvent && event.logicalKey == LogicalKeyboardKey.tab) { - log('_TextFieldAutoCompleteEmailAddressState::inputfieldBuilder(): PRESS TAB'); widget.nextFocusNode?.requestFocus(); } }, - child: TextField( + child: TextFieldBuilder( controller: tec, focusNode: fn, textInputAction: TextInputAction.next, + maxLines: 1, + textStyle: TextFieldAutocompleteEmailAddressStyle.inputTextStyle, decoration: InputDecoration( filled: true, - fillColor: AppColor.loginTextFieldBackgroundColor, + fillColor: Colors.white, contentPadding: const EdgeInsets.only( right: 8, left: 12, @@ -150,7 +152,7 @@ class _TextFieldAutoCompleteEmailAddressState Radius.circular(10), ), borderSide: BorderSide( - width: 0.5, + width: 1, color: AppColor.colorInputBorderCreateMailbox, ), ), @@ -158,29 +160,45 @@ class _TextFieldAutoCompleteEmailAddressState borderRadius: BorderRadius.all( Radius.circular(10), ), + borderSide: BorderSide( + width: 1, + color: AppColor.colorInputBorderCreateMailbox, + ), ), hintText: widget.advancedSearchFilterField.getHintText(context), hintStyle: const TextStyle( - fontSize: 14, + fontSize: 16, color: AppColor.colorHintSearchBar, ), prefixIconConstraints: BoxConstraints(maxWidth: _distanceToField * 0.74), - prefixIcon: tags.isNotEmpty ? SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 10), - controller: sc, - scrollDirection: Axis.horizontal, - child: Row( - children: tags.map((String tag) { - return _buildTagItem(context, tag, onTagDelete); - }).toList()), - ) - : null, + prefixIcon: tags.isNotEmpty + ? SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 10), + controller: sc, + scrollDirection: Axis.horizontal, + child: Row( + children: tags + .map((tagName) => AutocompleteTagItemWidget( + tagName: tagName, + onDeleteCallback: onTagDelete + )) + .toList() + ), + ) + : null, ), - onChanged: (value) { - onChanged?.call(value); + onTextChange: (value) { + if (value.trim().isNotEmpty) { + onChanged?.call(value); + } }, - onSubmitted: (tag) { - onSubmitted?.call(tag); + onTextSubmitted: (tag) { + if (tag.trim().isNotEmpty) { + onSubmitted?.call(tag); + fn.requestFocus(); + } else { + FocusScope.of(context).unfocus(); + } }, ), ); @@ -191,139 +209,10 @@ class _TextFieldAutoCompleteEmailAddressState ); } - Widget _buildSuggestionItem( - BuildContext context, - EmailAddress emailAddress, - ) { - return Container( - color: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12), - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - width: 40, - height: 40, - alignment: Alignment.center, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AppColor.avatarColor, - border: Border.all( - color: AppColor.colorShadowBgContentEmail, - width: 1.0)), - child: Text( - emailAddress.asString().isNotEmpty - ? emailAddress.asString()[0].toUpperCase() - : '', - style: const TextStyle( - color: Colors.black, - fontSize: 16, - fontWeight: FontWeight.w600))), - Expanded(child: Padding( - padding: const EdgeInsets.only(left: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - emailAddress.asString(), - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: const TextStyle( - color: Colors.black, - fontSize: 16, fontWeight: - FontWeight.normal)), - if (emailAddress.displayName.isNotEmpty && - emailAddress.emailAddress.isNotEmpty) - Text( - emailAddress.emailAddress, - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: const TextStyle( - color: AppColor.colorHintSearchBar, - fontSize: 13, - fontWeight: FontWeight.normal)) - ]), - )), - ]), - ); - } - - Widget _buildTagItem( - BuildContext context, - String tag, - Function(String tag) onTagDelete, - ) { - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(10), - ), - color: AppColor.colorBackgroundTagFilter.withOpacity(0.08), - ), - margin: const EdgeInsets.only(right: 8), - padding: const EdgeInsets.fromLTRB(8, 4, 4, 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 24, - height: 24, - alignment: Alignment.center, - decoration: BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: AppColor.mapGradientColor[_generateIndex(tag)], - ), - border: Border.all( - color: AppColor.colorShadowBgContentEmail, width: 0.21)), - child: Text(tag[0].toUpperCase(), - style: const TextStyle( - color: Colors.white, - fontSize: 8.57, - fontWeight: FontWeight.w600))), - const SizedBox(width: 4), - Text(tag, - maxLines: 1, - overflow: kIsWeb ? null : TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.black, - fontSize: 17, - fontWeight: FontWeight.normal)), - const SizedBox(width: 4), - InkWell( - onTap: () => onTagDelete.call(tag), - child: SvgPicture.asset( - _imagePaths.icClose, - width: 28, - height: 28, - fit: BoxFit.fill, - ), - ) - ], - ), - ); - } - - int _generateIndex(String tag) { - if (tag.isNotEmpty) { - final codeUnits = tag.codeUnits; - if (codeUnits.isNotEmpty) { - final sumCodeUnits = codeUnits.sum; - final index = sumCodeUnits % AppColor.mapGradientColor.length; - return index; - } - } - return 0; - } - double _getHeightSuggestionBox(int countItem, double heightItem) { final maxHeightList = countItem * heightItem; - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return maxHeightList > 250 ? 250 : maxHeightList; } else { if (_responsiveUtils.isLandscapeMobile(context)) { @@ -335,7 +224,7 @@ class _TextFieldAutoCompleteEmailAddressState } double get maxWidthSuggestionBox { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (_responsiveUtils.isTabletLarge(context)) { return 300; } else { diff --git a/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_item.dart b/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_item.dart index 5a218406ec..aa34865b25 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_item.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_item.dart @@ -4,8 +4,9 @@ import 'package:core/presentation/utils/style_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/widget/link_browser_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/app_dashboard/linagora_app.dart'; -import 'package:url_launcher/url_launcher.dart' as launcher; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; class AppGridDashboardItem extends StatelessWidget { final LinagoraApp app; @@ -16,45 +17,47 @@ class AppGridDashboardItem extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: InkWell( - onTap: _openApp, - child: Column(children: [ - app.iconName.endsWith("svg") - ? SvgPicture.asset( - _imagePaths.getConfigurationImagePath(app.iconName), - width: 56, - height: 56, - fit: BoxFit.fill) - : Image.asset( - _imagePaths.getConfigurationImagePath(app.iconName), - width: 56, - height: 56, - fit: BoxFit.fill), - Padding( - padding: const EdgeInsets.only(top: 8), - child: Text( - app.appName, - maxLines: 1, - textAlign: TextAlign.center, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: const TextStyle( - color: AppColor.colorNameEmail, - fontSize: 15 + return LinkBrowserWidget( + uri: app.appUri, + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => AppUtils.launchLink(app.appUri.toString()), + borderRadius: const BorderRadius.all(Radius.circular(16)), + hoverColor: AppColor.colorBgMailboxSelected, + child: Container( + width: 98, + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column(children: [ + app.iconName.endsWith("svg") + ? SvgPicture.asset( + _imagePaths.getConfigurationImagePath(app.iconName), + width: 56, + height: 56, + fit: BoxFit.fill) + : Image.asset( + _imagePaths.getConfigurationImagePath(app.iconName), + width: 56, + height: 56, + fit: BoxFit.fill), + Padding( + padding: const EdgeInsets.only(top: 8), + child: Text( + app.appName, + maxLines: 1, + textAlign: TextAlign.center, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + style: const TextStyle( + color: AppColor.colorNameEmail, + fontSize: 15 + ), + ), ), - ), + ]), ), - ]), + ) ) ); } - - void _openApp() async { - final url = app.appUri; - if (await launcher.canLaunchUrl(url)) { - await launcher.launchUrl(url); - } - } } diff --git a/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_overlay.dart b/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_overlay.dart index fb3f9baea3..b59cd974c9 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_overlay.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_overlay.dart @@ -1,5 +1,4 @@ import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/views/list/sliver_grid_delegate_fixed_height.dart'; import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/app_dashboard/linagora_applications.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_grid_dashboard_item.dart'; @@ -14,36 +13,38 @@ class AppDashboardOverlay extends StatelessWidget { return GestureDetector( onTap: () => FocusManager.instance.primaryFocus?.unfocus(), child: Container( - width: 342, - height: 244, + width: _widthAppGrid, decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(24), boxShadow: const [ BoxShadow( - color: AppColor.colorShadowComposer, - blurRadius: 32, + color: AppColor.colorShadowLayerBottom, + blurRadius: 96, offset: Offset.zero), BoxShadow( - color: AppColor.colorDropShadow, - blurRadius: 4, + color: AppColor.colorShadowLayerTop, + blurRadius: 2, offset: Offset.zero), ] ), - child: GridView.builder( - padding: const EdgeInsets.all(24), - itemBuilder: (context, i) { - final app = _linagoraApplications.apps[i]; - return AppGridDashboardItem(app); - }, - primary: true, - itemCount: _linagoraApplications.apps.length, - gridDelegate: const SliverGridDelegateFixedHeight( - height: 98, - crossAxisCount: 3, - ), - ), + padding: const EdgeInsets.all(24), + child: Wrap(children: _linagoraApplications.apps + .map((app) => AppGridDashboardItem(app)) + .toList()), ), ); } + + double get _widthAppGrid { + if (_linagoraApplications.apps.length >= 3) { + return 342; + } else if (_linagoraApplications.apps.length == 2) { + return 244; + } else if (_linagoraApplications.apps.length == 1) { + return 146; + } else { + return 0; + } + } } diff --git a/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_list_dashboard_item.dart b/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_list_dashboard_item.dart index 8b72224be7..b095237c65 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_list_dashboard_item.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/app_dashboard/app_list_dashboard_item.dart @@ -1,76 +1,58 @@ -import 'package:core/core.dart'; +import 'dart:io'; + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/text/slogan_builder.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:external_app_launcher/external_app_launcher.dart'; import 'package:flutter/material.dart'; import 'package:get/get_core/get_core.dart'; import 'package:get/get_instance/get_instance.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/app_dashboard/linagora_app.dart'; import 'package:url_launcher/url_launcher.dart' as launcher; -class AppListDashboardItem extends StatefulWidget { +class AppListDashboardItem extends StatelessWidget { + final LinagoraApp app; const AppListDashboardItem(this.app, {Key? key}) : super(key: key); - @override - State createState() => _AppListDashboardItemState(); -} - -class _AppListDashboardItemState extends State { - final _imagePaths = Get.find(); - final _responsiveUtils = Get.find(); - bool _isHoverItem = false; - @override Widget build(BuildContext context) { - return StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return InkWell( - onTap: () => _openApp(widget.app.appUri), - onHover: (value) => setState(() => _isHoverItem = value), - child: Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(10)), - color: _backgroundColorItem(context)), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 18), - margin: const EdgeInsets.symmetric(horizontal: 16), - child: _buildAppItem(context) - ), - ); - } + final imagePaths = Get.find(); + return SloganBuilder( + sizeLogo: 32, + paddingText: const EdgeInsetsDirectional.only(start: 12), + text: app.appName, + textAlign: TextAlign.center, + textStyle: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorNameEmail), + logo: !app.iconName.endsWith('svg') ? imagePaths.getConfigurationImagePath(app.iconName) : null, + logoSVG: app.iconName.endsWith('svg') ? imagePaths.getConfigurationImagePath(app.iconName) : null, + onTapCallback: () => _openApp(context, app), + padding: const EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 20), + hoverColor: AppColor.colorBgMailboxSelected ); } - Widget _buildAppItem(BuildContext context) { - final builder = SloganBuilder(arrangedByHorizontal: true) - ..setSizeLogo(32.0) - ..setPadding(const EdgeInsets.only(left: 12)) - ..setSloganText(widget.app.appName) - ..addOnTapCallback(() async { - final url = widget.app.appUri; - if (await launcher.canLaunchUrl(url)) { - await launcher.launchUrl(url); - } - }) - ..setSloganTextStyle(const TextStyle(fontSize: 17, fontWeight: FontWeight.w500, color: AppColor.colorNameEmail)); - if (widget.app.iconName.endsWith('svg')) { - builder.setLogoSVG(_imagePaths.getConfigurationImagePath(widget.app.iconName)); + void _openApp(BuildContext context, LinagoraApp app) async { + if (PlatformInfo.isWeb) { + if (await launcher.canLaunchUrl(app.appUri)) { + await launcher.launchUrl(app.appUri); + } + } else if (Platform.isAndroid && app.androidPackageId?.isNotEmpty == true) { + await LaunchApp.openApp(androidPackageName: app.androidPackageId); + } else if (Platform.isIOS && app.iosUrlScheme?.isNotEmpty == true) { + await LaunchApp.openApp( + iosUrlScheme: '${app.iosUrlScheme}://', + appStoreLink: app.iosAppStoreLink + ); } else { - builder.setLogo(_imagePaths.getConfigurationImagePath(widget.app.iconName)); - } - return builder.build(); - } - - Color _backgroundColorItem(BuildContext context) { - if (_isHoverItem) { - return AppColor.colorBgMailboxSelected; - } - return _responsiveUtils.isDesktop(context) - ? AppColor.colorBgDesktop - : Colors.white; - } - - void _openApp(Uri appLink) async { - if (await launcher.canLaunchUrl(appLink)) { - await launcher.launchUrl(appLink); + if (await launcher.canLaunchUrl(app.appUri)) { + await launcher.launchUrl( + app.appUri, + mode: launcher.LaunchMode.externalApplication + ); + } } } } diff --git a/lib/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart index d5e7c19d0b..af6eeca6c8 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/download/download_task_item_widget.dart @@ -19,7 +19,7 @@ class DownloadTaskItemWidget extends StatelessWidget with AppLoaderMixin { @override Widget build(BuildContext context) { - final _imagePaths = Get.find(); + final imagePaths = Get.find(); return Container( padding: EdgeInsets.zero, @@ -36,7 +36,7 @@ class DownloadTaskItemWidget extends StatelessWidget with AppLoaderMixin { height: 30, child: Stack(alignment: Alignment.center, children: [ SvgPicture.asset( - taskState.attachment.getIcon(_imagePaths), + taskState.attachment.getIcon(imagePaths), width: 16, height: 16, fit: BoxFit.fill), diff --git a/lib/features/mailbox_dashboard/presentation/widgets/email_quick_search_item_tile_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/email_quick_search_item_tile_widget.dart index a20ce6533c..747efe6872 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/email_quick_search_item_tile_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/email_quick_search_item_tile_widget.dart @@ -5,6 +5,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; class EmailQuickSearchItemTileWidget extends StatelessWidget { @@ -13,7 +14,7 @@ class EmailQuickSearchItemTileWidget extends StatelessWidget { final PresentationEmail _presentationEmail; final PresentationMailbox? _presentationMailbox; - final EdgeInsets? contentPadding; + final EdgeInsetsGeometry? contentPadding; EmailQuickSearchItemTileWidget( this._presentationEmail, @@ -29,7 +30,7 @@ class EmailQuickSearchItemTileWidget extends StatelessWidget { final maxWidthItem = constraints.maxWidth; log('EmailQuickSearchItemTileWidget::build(): maxWidthItem: $maxWidthItem'); return Padding( - padding: contentPadding ?? const EdgeInsets.all(12), + padding: contentPadding ?? const EdgeInsetsDirectional.all(12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, diff --git a/lib/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart index 5e112c5903..9d2364476c 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart @@ -10,7 +10,7 @@ class RecentSearchItemTileWidget extends StatelessWidget { final imagePath = Get.find(); final RecentSearch recentSearch; - final EdgeInsets? contentPadding; + final EdgeInsetsGeometry? contentPadding; RecentSearchItemTileWidget( this.recentSearch, { @@ -21,7 +21,7 @@ class RecentSearchItemTileWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: contentPadding ?? const EdgeInsets.all(12), + padding: contentPadding ?? const EdgeInsetsDirectional.all(12), child: Row( children: [ SvgPicture.asset(imagePath.icClockSB), diff --git a/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart index ca843498ab..c9f0b51bb0 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/search_input_form_widget.dart @@ -1,5 +1,10 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/presentation/views/quick_search/quick_search_input_form.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -16,108 +21,49 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/ad import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/email_quick_search_item_tile_widget.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/widgets/recent_search_item_tile_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { - final MailboxDashBoardController dashBoardController; - final ImagePaths imagePaths; - final double maxWidth; + final _searchController = Get.find(); + final _dashBoardController = Get.find(); + final _imagePaths = Get.find(); - SearchInputFormWidget({ - Key? key, - required this.dashBoardController, - required this.imagePaths, - required this.maxWidth, - }) : super(key: key); + SearchInputFormWidget({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - final controller = dashBoardController.searchController; - - return PortalTarget( - visible: controller.isAdvancedSearchViewOpen.isTrue, - portalFollower: PointerInterceptor( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => controller.selectOpenAdvanceSearch()), - ), - child: PortalTarget( - visible: controller.isAdvancedSearchViewOpen.isTrue, - anchor: const Aligned( - follower: Alignment.topRight, - target: Alignment.bottomRight, - widthFactor: 1, - backup: Aligned( + return Obx(() { + return PortalTarget( + visible: _searchController.isAdvancedSearchViewOpen.isTrue, + portalFollower: PointerInterceptor( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _searchController.selectOpenAdvanceSearch() + ), + ), + child: PortalTarget( + visible: _searchController.isAdvancedSearchViewOpen.isTrue, + anchor: const Aligned( follower: Alignment.topRight, target: Alignment.bottomRight, widthFactor: 1, + backup: Aligned( + follower: Alignment.topRight, + target: Alignment.bottomRight, + widthFactor: 1, + ), ), - ), - portalFollower: AdvancedSearchFilterOverlay(maxWidth: maxWidth), - child: QuickSearchInputForm( + portalFollower: const AdvancedSearchFilterOverlay(), + child: QuickSearchInputForm( maxHeight: 52, suggestionsBoxVerticalOffset: 0.0, - textFieldConfiguration: QuickSearchTextFieldConfiguration( - controller: controller.searchInputController, - autofocus: controller.autoFocus.value, - enabled: controller.isAdvancedSearchViewOpen.isFalse, - focusNode: controller.searchFocus, - textInputAction: TextInputAction.done, - onSubmitted: (keyword) { - final query = keyword.trim(); - if (query.isNotEmpty) { - controller.saveRecentSearch(RecentSearch.now(query)); - dashBoardController.searchEmail(context, query); - } else { - dashBoardController.clearSearchEmail(); - } - }, - onChanged: controller.onChangeTextSearch, - decoration: InputDecoration( - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - contentPadding: EdgeInsets.zero, - hintText: AppLocalizations.of(context).search_emails, - hintStyle: const TextStyle( - color: AppColor.colorHintSearchBar, - fontSize: 16.0), - labelStyle: const TextStyle( - color: Colors.black, - fontSize: 16.0) - ), - leftButton: Padding( - padding: const EdgeInsets.only(left: 8), - child: buildIconWeb( - minSize: 40, - iconPadding: EdgeInsets.zero, - icon: SvgPicture.asset( - imagePaths.icSearchBar, - fit: BoxFit.fill), - onTap: () { - final keyword = controller.searchInputController.text.trim(); - if (keyword.isNotEmpty) { - controller.saveRecentSearch(RecentSearch.now(keyword)); - dashBoardController.searchEmail(context, keyword); - } else { - dashBoardController.clearSearchEmail(); - } - } - ) - ), - clearTextButton: buildIconWeb( - icon: SvgPicture.asset( - imagePaths.icClearTextSearch, - width: 16, - height: 16, - fit: BoxFit.fill), - onTap: controller.clearTextSearch), - rightButton: IconOpenAdvancedSearchWidget(context) + textFieldConfiguration: _createConfiguration(context), + suggestionsBoxDecoration: const QuickSearchSuggestionsBoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(16)), ), - suggestionsBoxDecoration: QuickSearchSuggestionsBoxDecoration( - color: Colors.white, - borderRadius: const BorderRadius.all(Radius.circular(16)), - constraints: BoxConstraints(maxWidth: maxWidth)), - debounceDuration: const Duration(milliseconds: 500), + debounceDuration: const Duration(milliseconds: 300), listActionButton: QuickSearchFilter.values, actionButtonBuilder: (context, filterAction) { if (filterAction is QuickSearchFilter) { @@ -128,125 +74,174 @@ class SearchInputFormWidget extends StatelessWidget with AppLoaderMixin { }, buttonActionCallback: (filterAction) { if (filterAction is QuickSearchFilter) { - dashBoardController.selectQuickSearchFilter( - quickSearchFilter: filterAction, - fromSuggestionBox: true, - ); + _dashBoardController.addFilterToSuggestionForm(filterAction); } }, - listActionPadding: const EdgeInsets.only( - left: 12, - right: 12, - top: 12, - bottom: 6), + listActionPadding: const EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 6), titleHeaderRecent: Padding( - padding: const EdgeInsets.only( - left: 12, - right: 12, - bottom: 8, - top: 12), - child: Text(AppLocalizations.of(context).recent, - style: const TextStyle( - fontSize: 13.0, - color: AppColor.colorTextButtonHeaderThread, - fontWeight: FontWeight.w500) + padding: const EdgeInsets.only(left: 12, right: 12, bottom: 8, top: 12), + child: Text( + AppLocalizations.of(context).recent, + style: const TextStyle( + fontSize: 13.0, + color: AppColor.colorTextButtonHeaderThread, + fontWeight: FontWeight.w500 ) + ) ), buttonShowAllResult: (context, keyword) { if (keyword is String) { - return InkWell( - onTap: () { - final query = keyword.trim(); - if (query.isNotEmpty) { - controller.saveRecentSearch(RecentSearch.now(query)); - dashBoardController.searchEmail(context, query); - } else { - dashBoardController.clearSearchEmail(); - } - - }, - child: Padding( - padding: const EdgeInsets.all(12), - child: Row(children: [ - Text(AppLocalizations.of(context).showingResultsFor, - style: const TextStyle( - fontSize: 13.0, - color: AppColor.colorTextButtonHeaderThread, - fontWeight: FontWeight.w500)), - const SizedBox(width: 4), - Expanded(child: Text('"$keyword"', - style: const TextStyle( - fontSize: 13.0, - color: Colors.black, - fontWeight: FontWeight.w500))) - ]) - ), - ); + return _buildShowAllResultButton(context, keyword); } else { return const SizedBox.shrink(); } }, loadingBuilder: (context) => Padding( - padding: const EdgeInsets.only(bottom: 16), - child: loadingWidget), - fetchRecentActionCallback: controller.getAllRecentSearchAction, - itemRecentBuilder: (context, recent) => - RecentSearchItemTileWidget(recent), - onRecentSelected: (recent) { - controller.searchInputController.text = recent.value; - dashBoardController.searchEmail(context, recent.value); - }, - suggestionsCallback: (pattern) => - dashBoardController.quickSearchEmails(), - itemBuilder: (context, email) => - EmailQuickSearchItemTileWidget(email, dashBoardController.selectedMailbox.value), - onSuggestionSelected: (presentationEmail) => - dashBoardController.dispatchAction( - OpenEmailDetailedFromSuggestionQuickSearchAction( - context, - presentationEmail))), + padding: const EdgeInsets.only(bottom: 16), + child: loadingWidget + ), + fetchRecentActionCallback: _searchController.getAllRecentSearchAction, + itemRecentBuilder: (context, recent) => RecentSearchItemTileWidget(recent), + onRecentSelected: (recent) => _invokeSelectRecentItem(context, recent), + suggestionsCallback: _dashBoardController.quickSearchEmails, + itemBuilder: (context, email) => EmailQuickSearchItemTileWidget(email, _dashBoardController.selectedMailbox.value), + onSuggestionSelected: (presentationEmail) => _invokeSelectSuggestionItem(context, presentationEmail)) + ), + ); + }); + } + + void _invokeSearchEmailAction(BuildContext context, String query) { + _searchController.searchFocus.unfocus(); + _searchController.enableSearch(); + + if (query.isNotEmpty) { + _searchController.saveRecentSearch(RecentSearch.now(query)); + } + + if (query.isNotEmpty || _searchController.listFilterOnSuggestionForm.isNotEmpty) { + _searchController.applyFilterSuggestionToSearchFilter(_dashBoardController.userProfile.value); + _dashBoardController.searchEmail(context, queryString: query); + } else { + _dashBoardController.clearSearchEmail(); + } + } + + void _invokeSelectSuggestionItem(BuildContext context, PresentationEmail presentationEmail) { + _dashBoardController.dispatchAction( + OpenEmailDetailedFromSuggestionQuickSearchAction( + context, + presentationEmail + ) + ); + } + + void _invokeSelectRecentItem(BuildContext context, RecentSearch recent) { + _searchController.searchInputController.text = recent.value; + _searchController.searchFocus.unfocus(); + _searchController.enableSearch(); + + _searchController.applyFilterSuggestionToSearchFilter(_dashBoardController.userProfile.value); + _dashBoardController.searchEmail(context, queryString: recent.value); + } + + Widget _buildShowAllResultButton(BuildContext context, String keyword) { + return InkWell( + onTap: () => _invokeSearchEmailAction(context, keyword.trim()), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row(children: [ + Text( + AppLocalizations.of(context).showingResultsFor, + style: const TextStyle( + fontSize: 13.0, + color: AppColor.colorTextButtonHeaderThread, + fontWeight: FontWeight.w500 + ) + ), + const SizedBox(width: 4), + Expanded(child: Text( + '"$keyword"', + style: const TextStyle( + fontSize: 13.0, + color: Colors.black, + fontWeight: FontWeight.w500 + ) + )) + ]) ), ); } - Widget buildListButtonForQuickSearchForm(BuildContext context, QuickSearchFilter filter) { - final controller = dashBoardController.searchController; + QuickSearchTextFieldConfiguration _createConfiguration(BuildContext context) { + return QuickSearchTextFieldConfiguration( + controller: _searchController.searchInputController, + focusNode: _searchController.searchFocus, + textInputAction: TextInputAction.done, + textDirection: DirectionUtils.getDirectionByLanguage(context), + onSubmitted: (keyword) => _invokeSearchEmailAction(context, keyword.trim()), + decoration: InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, + hintText: AppLocalizations.of(context).search_emails, + hintStyle: const TextStyle(color: AppColor.colorHintSearchBar, fontSize: 16.0), + labelStyle: const TextStyle(color: Colors.black, fontSize: 16.0) + ), + leftButton: Padding( + padding: EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 0 : 8, + right: AppUtils.isDirectionRTL(context) ? 8 : 0 + ), + child: buildIconWeb( + minSize: 40, + iconPadding: EdgeInsets.zero, + icon: SvgPicture.asset(_imagePaths.icSearchBar, fit: BoxFit.fill), + onTap: () => _invokeSearchEmailAction(context, _searchController.searchInputController.text.trim()) + ) + ), + clearTextButton: buildIconWeb( + icon: SvgPicture.asset( + _imagePaths.icClearTextSearch, + width: 16, + height: 16, + fit: BoxFit.fill + ), + onTap: _searchController.clearTextSearch + ), + rightButton: IconOpenAdvancedSearchWidget(context) + ); + } + Widget buildListButtonForQuickSearchForm(BuildContext context, QuickSearchFilter filter) { return Obx(() { - final isFilterSelected = dashBoardController.checkQuickSearchFilterSelected( - quickSearchFilter: filter); + final isFilterSelected = filter.isApplied(_searchController.listFilterOnSuggestionForm); return Chip( - labelPadding: const EdgeInsets.only( - top: 2, - bottom: 2, - right: 10), + labelPadding: EdgeInsets.only( + top: 2, + bottom: 2, + right: AppUtils.isDirectionRTL(context) ? 0 : 10, + left: AppUtils.isDirectionRTL(context) ? 10 : 0, + ), label: Text( - filter.getTitle( - context, - receiveTimeType: controller.emailReceiveTimeType.value), + filter.getName(context), maxLines: 1, overflow: CommonTextStyle.defaultTextOverFlow, softWrap: CommonTextStyle.defaultSoftWrap, - style: filter.getTextStyle( - quickSearchFilterSelected: isFilterSelected), + style: filter.getTextStyle(isFilterSelected: isFilterSelected), ), avatar: SvgPicture.asset( - filter.getIcon( - imagePaths, - quickSearchFilterSelected: isFilterSelected), - width: 16, - height: 16, - fit: BoxFit.fill), - labelStyle: filter.getTextStyle( - quickSearchFilterSelected: isFilterSelected), - backgroundColor: filter.getBackgroundColor( - quickSearchFilterSelected: isFilterSelected), + filter.getIcon(_imagePaths, isFilterSelected: isFilterSelected), + width: 16, + height: 16, + fit: BoxFit.fill), + labelStyle: filter.getTextStyle(isFilterSelected: isFilterSelected), + backgroundColor: filter.getBackgroundColor(isFilterSelected: isFilterSelected), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), - side: BorderSide( - color: filter.getBackgroundColor( - quickSearchFilterSelected: isFilterSelected)), + side: BorderSide(color: filter.getBackgroundColor(isFilterSelected: isFilterSelected)), ), ); }); diff --git a/lib/features/mailbox_dashboard/presentation/widgets/spam_report_banner_web_widget.dart b/lib/features/mailbox_dashboard/presentation/widgets/spam_report_banner_web_widget.dart deleted file mode 100644 index 1b0faea2dc..0000000000 --- a/lib/features/mailbox_dashboard/presentation/widgets/spam_report_banner_web_widget.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/views/button/icon_button_web.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -class SpamReportBannerWebWidget extends StatelessWidget { - const SpamReportBannerWebWidget({ Key? key }) : super(key: key); - - @override - Widget build(BuildContext context){ - final _spamReportController = Get.find(); - final _imagePaths = Get.find(); - return Obx(() { - if (!_spamReportController.enableSpamReport || _spamReportController.notShowSpamReportBanner) { - return const SizedBox( - height: 8, - ); - } - return Container( - height: 84, - margin: const EdgeInsets.only(right: 16, top: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColor.colorBorderBodyThread, width: 1), - color: AppColor.colorSpamReportBox.withOpacity(0.12)), - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - children: [ - SvgPicture.asset( - _imagePaths.icInfoCircleOutline, - width: 28, - height: 28, - color: AppColor.primaryColor, - ), - const SizedBox(width: 8), - Text( - AppLocalizations.of(context).countNewSpamEmails( - _spamReportController.numberOfUnreadSpamEmails), - style: const TextStyle( - fontSize: 16, - color: AppColor.primaryColor, - fontWeight: FontWeight.w500), - ), - ], - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - ), - child: buildTextButton( - AppLocalizations.of(context).showDetails, - height: 36, - width: 115, - textStyle: const TextStyle( - fontSize: 15, - color: AppColor.primaryColor, - fontWeight: FontWeight.w400), - backgroundColor: AppColor.colorCreateNewIdentityButton, - radius: 10, - onTap: () => _spamReportController.openMailbox(context), - ), - ), - ], - ), - Positioned( - top: 16, - right: 16, - child: buildSVGIconButton( - icon: _imagePaths.icCloseComposer, - onTap: () => _spamReportController.dismissSpamReportAction(), - ), - ), - ], - ), - ); - }); - } -} \ No newline at end of file diff --git a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart index 0bdf165cee..1fc685714d 100644 --- a/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart +++ b/lib/features/mailbox_dashboard/presentation/widgets/top_bar_thread_selection.dart @@ -41,8 +41,8 @@ class TopBarThreadSelection { return Row(children: [ buildIconWeb( icon: SvgPicture.asset( - imagePaths.icCloseComposer, - color: AppColor.colorTextButton, + imagePaths.icClose, + colorFilter: AppColor.colorTextButton.asFilter(), fit: BoxFit.fill), tooltip: AppLocalizations.of(context).cancel, onTap: onCancelSelection), @@ -113,9 +113,9 @@ class TopBarThreadSelection { canDeletePermanently ? imagePaths.icDeleteComposer : imagePaths.icDelete, - color: canDeletePermanently - ? AppColor.colorDeletePermanentlyButton - : AppColor.primaryColor, + colorFilter: canDeletePermanently + ? AppColor.colorDeletePermanentlyButton.asFilter() + : AppColor.primaryColor.asFilter(), width: 20, height: 20, fit: BoxFit.fill), diff --git a/lib/features/mailto/presentation/mailto_url_bindings.dart b/lib/features/mailto/presentation/mailto_url_bindings.dart new file mode 100644 index 0000000000..22264cf580 --- /dev/null +++ b/lib/features/mailto/presentation/mailto_url_bindings.dart @@ -0,0 +1,34 @@ +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/base_bindings.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; +import 'package:tmail_ui_user/features/mailto/presentation/mailto_url_controller.dart'; + +class MailtoUrlBindings extends BaseBindings { + + @override + void bindingsController() { + Get.lazyPut(() => MailtoUrlController( + Get.find(), + Get.find(), + )); + } + + @override + void bindingsDataSource() {} + + @override + void bindingsDataSourceImpl() {} + + @override + void bindingsInteractor() { + Get.lazyPut(() => UpdateAuthenticationAccountInteractor(Get.find())); + } + + @override + void bindingsRepository() {} + + @override + void bindingsRepositoryImpl() {} +} \ No newline at end of file diff --git a/lib/features/mailto/presentation/mailto_url_controller.dart b/lib/features/mailto/presentation/mailto_url_controller.dart new file mode 100644 index 0000000000..9953d08aa8 --- /dev/null +++ b/lib/features/mailto/presentation/mailto_url_controller.dart @@ -0,0 +1,55 @@ +import 'package:core/utils/app_logger.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; +import 'package:tmail_ui_user/features/mailto/presentation/model/mailto_arguments.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/navigation_router.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/main/routes/route_utils.dart'; + +class MailtoUrlController extends ReloadableController { + + MailtoUrlController( + GetAuthenticatedAccountInteractor getAuthenticatedAccountInteractor, + UpdateAuthenticationAccountInteractor updateAuthenticationAccountInteractor, + ) : super( + getAuthenticatedAccountInteractor, + updateAuthenticationAccountInteractor + ); + + @override + void onReady() { + super.onReady(); + final parameters = Get.parameters; + log('MailtoUrlController::onReady():parameters: $parameters'); + if (parameters.containsKey('uri')) { + reload(); + } else { + popAndPush(AppRoutes.unknownRoutePage); + } + } + + @override + void handleReloaded(Session session) { + final parameters = Get.parameters; + log('MailtoUrlController::handleReloaded():parameters: $parameters'); + if (parameters.containsKey('uri')) { + final mailtoArgument = MailtoArguments( + session: session, + mailtoUri: parameters['uri'] + ); + popAndPush( + RouteUtils.generateNavigationRoute( + AppRoutes.dashboard, + NavigationRouter.initial() + ), + arguments: mailtoArgument + ); + } else { + popAndPush(AppRoutes.unknownRoutePage); + } + } +} \ No newline at end of file diff --git a/lib/features/mailto/presentation/mailto_url_view.dart b/lib/features/mailto/presentation/mailto_url_view.dart new file mode 100644 index 0000000000..35bf853f05 --- /dev/null +++ b/lib/features/mailto/presentation/mailto_url_view.dart @@ -0,0 +1,23 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/theme_utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/mailto/presentation/mailto_url_controller.dart'; + +class MailtoUrlView extends GetWidget { + const MailtoUrlView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + ThemeUtils.setSystemDarkUIStyle(); + + return Container( + color: AppColor.primaryLightColor, + child: const SizedBox( + width: 100, + height: 100, + child: CupertinoActivityIndicator(), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/mailto/presentation/model/mailto_arguments.dart b/lib/features/mailto/presentation/model/mailto_arguments.dart new file mode 100644 index 0000000000..ec7d538e18 --- /dev/null +++ b/lib/features/mailto/presentation/model/mailto_arguments.dart @@ -0,0 +1,17 @@ + +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/main/routes/route_utils.dart'; +import 'package:tmail_ui_user/main/routes/router_arguments.dart'; + +class MailtoArguments extends RouterArguments { + + final Session session; + final String? mailtoUri; + + MailtoArguments({required this.session, this.mailtoUri}); + + Map toMapRouter() => RouteUtils.parseMapMailtoFromUri(mailtoUri); + + @override + List get props => [session, mailtoUri]; +} \ No newline at end of file diff --git a/lib/features/manage_account/data/datasource/identity_data_source.dart b/lib/features/manage_account/data/datasource/identity_data_source.dart index 0160fd21dc..a7fc8f3bba 100644 --- a/lib/features/manage_account/data/datasource/identity_data_source.dart +++ b/lib/features/manage_account/data/datasource/identity_data_source.dart @@ -1,16 +1,19 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/edit_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/identities_response.dart'; abstract class IdentityDataSource { - Future getAllIdentities(AccountId accountId, {Properties? properties}); + Future getAllIdentities(Session session, AccountId accountId, {Properties? properties}); - Future createNewIdentity(AccountId accountId, CreateNewIdentityRequest identityRequest); + Future createNewIdentity(Session session, AccountId accountId, CreateNewIdentityRequest identityRequest); - Future deleteIdentity(AccountId accountId, IdentityId identityId); + Future deleteIdentity(Session session, AccountId accountId, IdentityId identityId); - Future editIdentity(AccountId accountId, EditIdentityRequest editIdentityRequest); + Future editIdentity(Session session, AccountId accountId, EditIdentityRequest editIdentityRequest); + + Future transformHtmlSignature(String signature); } \ No newline at end of file diff --git a/lib/features/manage_account/data/datasource_impl/forwarding_data_source_impl.dart b/lib/features/manage_account/data/datasource_impl/forwarding_data_source_impl.dart index 57bb7f200c..f367b54ef4 100644 --- a/lib/features/manage_account/data/datasource_impl/forwarding_data_source_impl.dart +++ b/lib/features/manage_account/data/datasource_impl/forwarding_data_source_impl.dart @@ -18,35 +18,27 @@ class ForwardingDataSourceImpl extends ForwardingDataSource { Future getForward(AccountId accountId) { return Future.sync(() async { return await _forwardingAPI.getForward(accountId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future deleteRecipientInForwarding(AccountId accountId, DeleteRecipientInForwardingRequest deleteRequest) { return Future.sync(() async { return await _forwardingAPI.updateForward(accountId, deleteRequest.newTMailForward); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future addRecipientsInForwarding(AccountId accountId, AddRecipientInForwardingRequest addRequest) { return Future.sync(() async { return await _forwardingAPI.updateForward(accountId, addRequest.newTMailForward); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future editLocalCopyInForwarding(AccountId accountId, EditLocalCopyInForwardingRequest editRequest) { return Future.sync(() async { return await _forwardingAPI.updateForward(accountId, editRequest.newTMailForward); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/manage_account/data/datasource_impl/identity_data_source_impl.dart b/lib/features/manage_account/data/datasource_impl/identity_data_source_impl.dart index 4cfb232f64..7cbf828728 100644 --- a/lib/features/manage_account/data/datasource_impl/identity_data_source_impl.dart +++ b/lib/features/manage_account/data/datasource_impl/identity_data_source_impl.dart @@ -1,5 +1,15 @@ +import 'package:core/presentation/utils/html_transformer/dom/add_target_blank_in_tag_a_transformers.dart'; +import 'package:core/presentation/utils/html_transformer/dom/add_tooltip_link_transformers.dart'; +import 'package:core/presentation/utils/html_transformer/dom/blockcode_transformers.dart'; +import 'package:core/presentation/utils/html_transformer/dom/blockquoted_transformers.dart'; +import 'package:core/presentation/utils/html_transformer/dom/image_transformers.dart'; +import 'package:core/presentation/utils/html_transformer/dom/script_transformers.dart'; +import 'package:core/presentation/utils/html_transformer/html_transform.dart'; +import 'package:core/presentation/utils/html_transformer/transform_configuration.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:tmail_ui_user/features/manage_account/data/datasource/identity_data_source.dart'; import 'package:tmail_ui_user/features/manage_account/data/network/identity_api.dart'; @@ -10,45 +20,58 @@ import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; class IdentityDataSourceImpl extends IdentityDataSource { + final HtmlTransform _htmlTransform; final IdentityAPI _identityAPI; final ExceptionThrower _exceptionThrower; - IdentityDataSourceImpl(this._identityAPI, this._exceptionThrower); + IdentityDataSourceImpl( + this._htmlTransform, + this._identityAPI, + this._exceptionThrower + ); @override - Future getAllIdentities(AccountId accountId, - {Properties? properties}) { + Future getAllIdentities(Session session, AccountId accountId, {Properties? properties}) { return Future.sync(() async { - return await _identityAPI.getAllIdentities(accountId, properties: properties); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await _identityAPI.getAllIdentities(session, accountId, properties: properties); + }).catchError(_exceptionThrower.throwException); } @override - Future createNewIdentity(AccountId accountId, CreateNewIdentityRequest identityRequest) { + Future createNewIdentity(Session session, AccountId accountId, CreateNewIdentityRequest identityRequest) { return Future.sync(() async { - return await _identityAPI.createNewIdentity(accountId, identityRequest); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await _identityAPI.createNewIdentity(session, accountId, identityRequest); + }).catchError(_exceptionThrower.throwException); } @override - Future deleteIdentity(AccountId accountId, IdentityId identityId) { + Future deleteIdentity(Session session, AccountId accountId, IdentityId identityId) { return Future.sync(() async { - return await _identityAPI.deleteIdentity(accountId, identityId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await _identityAPI.deleteIdentity(session, accountId, identityId); + }).catchError(_exceptionThrower.throwException); } @override - Future editIdentity(AccountId accountId, EditIdentityRequest editIdentityRequest) { + Future editIdentity(Session session, AccountId accountId, EditIdentityRequest editIdentityRequest) { return Future.sync(() async { - return await _identityAPI.editIdentity(accountId, editIdentityRequest); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await _identityAPI.editIdentity(session, accountId, editIdentityRequest); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future transformHtmlSignature(String signature) { + return Future.sync(() async { + final signatureUnescape = await _htmlTransform.transformToHtml( + htmlContent: signature, + transformConfiguration: TransformConfiguration.create(customDomTransformers: [ + const RemoveScriptTransformer(), + const BlockQuotedTransformer(), + const BlockCodeTransformer(), + const AddTargetBlankInTagATransformer(), + const ImageTransformer(), + if (PlatformInfo.isWeb) const AddTooltipLinkTransformer() + ])); + return signatureUnescape; + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/manage_account/data/datasource_impl/manage_account_datasource_impl.dart b/lib/features/manage_account/data/datasource_impl/manage_account_datasource_impl.dart index f395927bb0..c9fbdfbb36 100644 --- a/lib/features/manage_account/data/datasource_impl/manage_account_datasource_impl.dart +++ b/lib/features/manage_account/data/datasource_impl/manage_account_datasource_impl.dart @@ -18,8 +18,6 @@ class ManageAccountDataSourceImpl extends ManageAccountDataSource { Future persistLanguage(Locale localeCurrent) { return Future.sync(() async { return await _languageCacheManager.persistLanguage(localeCurrent); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/manage_account/data/datasource_impl/rule_filter_datasource_impl.dart b/lib/features/manage_account/data/datasource_impl/rule_filter_datasource_impl.dart index 8f6909cde4..10e6ac7fe9 100644 --- a/lib/features/manage_account/data/datasource_impl/rule_filter_datasource_impl.dart +++ b/lib/features/manage_account/data/datasource_impl/rule_filter_datasource_impl.dart @@ -22,9 +22,7 @@ class RuleFilterDataSourceImpl extends RuleFilterDataSource { Future> getAllTMailRule(AccountId accountId) { return Future.sync(() async { return await _ruleFilterAPI.getListTMailRule(accountId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override @@ -34,26 +32,20 @@ class RuleFilterDataSourceImpl extends RuleFilterDataSource { return Future.sync(() async { return await _ruleFilterAPI.updateListTMailRule(accountId, deleteEmailRuleRequest.currentEmailRules); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future> createNewEmailRuleFilter(AccountId accountId, CreateNewEmailRuleFilterRequest ruleFilterRequest) { return Future.sync(() async { return await _ruleFilterAPI.updateListTMailRule(accountId, ruleFilterRequest.newListTMailRules); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future> editEmailRuleFilter(AccountId accountId, EditEmailRuleFilterRequest ruleFilterRequest) { return Future.sync(() async { return await _ruleFilterAPI.updateListTMailRule(accountId, ruleFilterRequest.listTMailRulesUpdated); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/manage_account/data/datasource_impl/vacation_data_source_impl.dart b/lib/features/manage_account/data/datasource_impl/vacation_data_source_impl.dart index d49ef9ec09..c6a1d0b1e1 100644 --- a/lib/features/manage_account/data/datasource_impl/vacation_data_source_impl.dart +++ b/lib/features/manage_account/data/datasource_impl/vacation_data_source_impl.dart @@ -15,17 +15,13 @@ class VacationDataSourceImpl extends VacationDataSource { Future> getAllVacationResponse(AccountId accountId) { return Future.sync(() async { return await _vacationAPI.getAllVacationResponse(accountId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future> updateVacation(AccountId accountId, VacationResponse vacationResponse) { return Future.sync(() async { return await _vacationAPI.updateVacation(accountId, vacationResponse); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/manage_account/data/network/identity_api.dart b/lib/features/manage_account/data/network/identity_api.dart index a1dbb1dc0a..39ee724d6b 100644 --- a/lib/features/manage_account/data/network/identity_api.dart +++ b/lib/features/manage_account/data/network/identity_api.dart @@ -1,7 +1,10 @@ + import 'package:jmap_dart_client/http/http_client.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/patch_object.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/identities/get/get_identity_method.dart'; import 'package:jmap_dart_client/jmap/identities/get/get_identity_response.dart'; @@ -9,34 +12,35 @@ import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:jmap_dart_client/jmap/identities/set/set_identity_method.dart'; import 'package:jmap_dart_client/jmap/identities/set/set_identity_response.dart'; import 'package:jmap_dart_client/jmap/jmap_request.dart'; -import 'package:model/identity/identity_request_dto.dart'; +import 'package:model/extensions/list_identity_id_extension.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_default_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/edit_default_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/edit_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/identities_response.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; class IdentityAPI { final HttpClient _httpClient; IdentityAPI(this._httpClient); - Future getAllIdentities(AccountId accountId, {Properties? properties}) async { + Future getAllIdentities(Session session, AccountId accountId, {Properties? properties}) async { final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); final getIdentityMethod = GetIdentityMethod(accountId); - var capability = getIdentityMethod.requiredCapabilities; if (properties != null) { getIdentityMethod.addProperties(properties); - if(properties.value.contains('sortOrder')) { - capability = getIdentityMethod.requiredCapabilitiesSupportSortOrder; - } - } else { - capability = getIdentityMethod.requiredCapabilitiesSupportSortOrder; } + + final jamesSortOrderIsSupported = [CapabilityIdentifier.jamesSortOrder].isSupported(session, accountId); + final capabilitySupported = jamesSortOrderIsSupported + ? getIdentityMethod.requiredCapabilitiesSupportSortOrder + : getIdentityMethod.requiredCapabilities; + final queryInvocation = requestBuilder.invocation(getIdentityMethod); final result = await (requestBuilder - ..usings(capability)) + ..usings(capabilitySupported)) .build() .execute(); @@ -47,14 +51,22 @@ class IdentityAPI { return IdentitiesResponse(identities: response?.list, state: response?.state); } - Future createNewIdentity(AccountId accountId, CreateNewIdentityRequest identityRequest) async { + Future createNewIdentity(Session session, AccountId accountId, CreateNewIdentityRequest identityRequest) async { final setIdentityMethod = SetIdentityMethod(accountId) ..addCreate(identityRequest.creationId, identityRequest.newIdentity); - - var capabilities = setIdentityMethod.requiredCapabilities; - if (identityRequest is CreateNewDefaultIdentityRequest) { - capabilities = setIdentityMethod.requiredCapabilitiesSupportSortOrder; - _addUpdatesToCreateDefaultIdentityMethod(setIdentityMethod, identityRequest); + + final jamesSortOrderIsSupported = [CapabilityIdentifier.jamesSortOrder].isSupported(session, accountId); + final capabilitySupported = jamesSortOrderIsSupported + ? setIdentityMethod.requiredCapabilitiesSupportSortOrder + : setIdentityMethod.requiredCapabilities; + + if (jamesSortOrderIsSupported && + identityRequest is CreateNewDefaultIdentityRequest && + identityRequest.oldDefaultIdentityIds != null + ) { + setIdentityMethod.addUpdates( + identityRequest.oldDefaultIdentityIds!.generateMapUpdateObjectSortOrder(sortOrder: UnsignedInt(100)) + ); } final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); @@ -62,7 +74,7 @@ class IdentityAPI { final setIdentityInvocation = requestBuilder.invocation(setIdentityMethod); final response = await (requestBuilder - ..usings(capabilities)) + ..usings(capabilitySupported)) .build() .execute(); @@ -73,27 +85,21 @@ class IdentityAPI { return setIdentityResponse!.created![identityRequest.creationId]!; } - void _addUpdatesToCreateDefaultIdentityMethod( - SetIdentityMethod setIdentityMethod, - CreateNewDefaultIdentityRequest identityRequest - ) { - for (var i = 0; i < (identityRequest.oldDefaultIdentityIds?.length ?? 0); i++) { - setIdentityMethod.addUpdates({ - identityRequest.oldDefaultIdentityIds![i].id : PatchObject(IdentityRequestDto(sortOrder: UnsignedInt(100)).toJson()) - }); - } - } - - Future deleteIdentity(AccountId accountId, IdentityId identityId) async { + Future deleteIdentity(Session session, AccountId accountId, IdentityId identityId) async { final setIdentityMethod = SetIdentityMethod(accountId) ..addDestroy({identityId.id}); + final jamesSortOrderIsSupported = [CapabilityIdentifier.jamesSortOrder].isSupported(session, accountId); + final capabilitySupported = jamesSortOrderIsSupported + ? setIdentityMethod.requiredCapabilitiesSupportSortOrder + : setIdentityMethod.requiredCapabilities; + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); final setIdentityInvocation = requestBuilder.invocation(setIdentityMethod); final response = await (requestBuilder - ..usings(setIdentityMethod.requiredCapabilities)) + ..usings(capabilitySupported)) .build() .execute(); @@ -104,21 +110,24 @@ class IdentityAPI { return setIdentityResponse?.destroyed?.contains(identityId.id) == true; } - Future editIdentity(AccountId accountId, EditIdentityRequest editIdentityRequest) async { + Future editIdentity(Session session, AccountId accountId, EditIdentityRequest editIdentityRequest) async { final setIdentityMethod = SetIdentityMethod(accountId) ..addUpdates({ editIdentityRequest.identityId.id : PatchObject(editIdentityRequest.identityRequest.toJson()) }); + + final jamesSortOrderIsSupported = [CapabilityIdentifier.jamesSortOrder].isSupported(session, accountId); + final capabilitySupported = jamesSortOrderIsSupported + ? setIdentityMethod.requiredCapabilitiesSupportSortOrder + : setIdentityMethod.requiredCapabilities; - var capabilities = setIdentityMethod.requiredCapabilities; - - if (editIdentityRequest is EditDefaultIdentityRequest) { - for (var identityId in editIdentityRequest.oldDefaultIdentityIds ?? []) { - setIdentityMethod.addUpdates({ - identityId.id: PatchObject(IdentityRequestDto(sortOrder: UnsignedInt(100)).toJson()) - }); - } - capabilities = setIdentityMethod.requiredCapabilitiesSupportSortOrder; + if (jamesSortOrderIsSupported && + editIdentityRequest is EditDefaultIdentityRequest && + editIdentityRequest.oldDefaultIdentityIds != null + ) { + setIdentityMethod.addUpdates( + editIdentityRequest.oldDefaultIdentityIds!.generateMapUpdateObjectSortOrder(sortOrder: UnsignedInt(100)) + ); } final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); @@ -126,7 +135,7 @@ class IdentityAPI { final setIdentityInvocation = requestBuilder.invocation(setIdentityMethod); final response = await (requestBuilder - ..usings(capabilities)) + ..usings(capabilitySupported)) .build() .execute(); diff --git a/lib/features/manage_account/data/repository/identity_repository_impl.dart b/lib/features/manage_account/data/repository/identity_repository_impl.dart index 427736846f..dc47af63da 100644 --- a/lib/features/manage_account/data/repository/identity_repository_impl.dart +++ b/lib/features/manage_account/data/repository/identity_repository_impl.dart @@ -1,6 +1,7 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:tmail_ui_user/features/manage_account/data/datasource/identity_data_source.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_identity_request.dart'; @@ -15,22 +16,27 @@ class IdentityRepositoryImpl extends IdentityRepository { IdentityRepositoryImpl(this._dataSource); @override - Future getAllIdentities(AccountId accountId, {Properties? properties}) { - return _dataSource.getAllIdentities(accountId, properties: properties); + Future getAllIdentities(Session session, AccountId accountId, {Properties? properties}) { + return _dataSource.getAllIdentities(session, accountId, properties: properties); } @override - Future createNewIdentity(AccountId accountId, CreateNewIdentityRequest identityRequest) { - return _dataSource.createNewIdentity(accountId, identityRequest); + Future createNewIdentity(Session session, AccountId accountId, CreateNewIdentityRequest identityRequest) { + return _dataSource.createNewIdentity(session, accountId, identityRequest); } @override - Future deleteIdentity(AccountId accountId, IdentityId identityId) { - return _dataSource.deleteIdentity(accountId, identityId); + Future deleteIdentity(Session session, AccountId accountId, IdentityId identityId) { + return _dataSource.deleteIdentity(session, accountId, identityId); } @override - Future editIdentity(AccountId accountId, EditIdentityRequest editIdentityRequest) { - return _dataSource.editIdentity(accountId, editIdentityRequest); + Future editIdentity(Session session, AccountId accountId, EditIdentityRequest editIdentityRequest) { + return _dataSource.editIdentity(session, accountId, editIdentityRequest); + } + + @override + Future transformHtmlSignature(String signature) { + return _dataSource.transformHtmlSignature(signature); } } \ No newline at end of file diff --git a/lib/features/manage_account/domain/repository/identity_repository.dart b/lib/features/manage_account/domain/repository/identity_repository.dart index a424d46d63..494ec9bbe1 100644 --- a/lib/features/manage_account/domain/repository/identity_repository.dart +++ b/lib/features/manage_account/domain/repository/identity_repository.dart @@ -1,16 +1,19 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/edit_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/identities_response.dart'; abstract class IdentityRepository { - Future getAllIdentities(AccountId accountId, {Properties? properties}); + Future getAllIdentities(Session session, AccountId accountId, {Properties? properties}); - Future createNewIdentity(AccountId accountId, CreateNewIdentityRequest identityRequest); + Future createNewIdentity(Session session, AccountId accountId, CreateNewIdentityRequest identityRequest); - Future deleteIdentity(AccountId accountId, IdentityId identityId); + Future deleteIdentity(Session session, AccountId accountId, IdentityId identityId); - Future editIdentity(AccountId accountId, EditIdentityRequest editIdentityRequest); + Future editIdentity(Session session, AccountId accountId, EditIdentityRequest editIdentityRequest); + + Future transformHtmlSignature(String signature); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/add_recipient_in_forwarding_state.dart b/lib/features/manage_account/domain/state/add_recipient_in_forwarding_state.dart index b47b00b555..8a957c1f83 100644 --- a/lib/features/manage_account/domain/state/add_recipient_in_forwarding_state.dart +++ b/lib/features/manage_account/domain/state/add_recipient_in_forwarding_state.dart @@ -12,10 +12,6 @@ class AddRecipientsInForwardingSuccess extends UIState { } class AddRecipientsInForwardingFailure extends FeatureFailure { - final dynamic exception; - AddRecipientsInForwardingFailure(this.exception); - - @override - List get props => [exception]; + AddRecipientsInForwardingFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/create_new_default_identity_state.dart b/lib/features/manage_account/domain/state/create_new_default_identity_state.dart index 639c08b78e..3d6c675227 100644 --- a/lib/features/manage_account/domain/state/create_new_default_identity_state.dart +++ b/lib/features/manage_account/domain/state/create_new_default_identity_state.dart @@ -14,10 +14,6 @@ class CreateNewDefaultIdentitySuccess extends UIState { } class CreateNewDefaultIdentityFailure extends FeatureFailure { - final dynamic exception; - CreateNewDefaultIdentityFailure(this.exception); - - @override - List get props => [exception]; + CreateNewDefaultIdentityFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/create_new_identity_state.dart b/lib/features/manage_account/domain/state/create_new_identity_state.dart index ce8794f170..612f91efac 100644 --- a/lib/features/manage_account/domain/state/create_new_identity_state.dart +++ b/lib/features/manage_account/domain/state/create_new_identity_state.dart @@ -14,10 +14,6 @@ class CreateNewIdentitySuccess extends UIState { } class CreateNewIdentityFailure extends FeatureFailure { - final dynamic exception; - CreateNewIdentityFailure(this.exception); - - @override - List get props => [exception]; + CreateNewIdentityFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/create_new_rule_filter_state.dart b/lib/features/manage_account/domain/state/create_new_rule_filter_state.dart index 7ea0758a5a..81f484d9f2 100644 --- a/lib/features/manage_account/domain/state/create_new_rule_filter_state.dart +++ b/lib/features/manage_account/domain/state/create_new_rule_filter_state.dart @@ -13,10 +13,6 @@ class CreateNewRuleFilterSuccess extends UIState { } class CreateNewRuleFilterFailure extends FeatureFailure { - final dynamic exception; - CreateNewRuleFilterFailure(this.exception); - - @override - List get props => [exception]; + CreateNewRuleFilterFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/delete_email_rule_state.dart b/lib/features/manage_account/domain/state/delete_email_rule_state.dart index 8f99bafdc6..bb8ee9d757 100644 --- a/lib/features/manage_account/domain/state/delete_email_rule_state.dart +++ b/lib/features/manage_account/domain/state/delete_email_rule_state.dart @@ -11,10 +11,6 @@ class DeleteEmailRuleSuccess extends UIState { } class DeleteEmailRuleFailure extends FeatureFailure { - final dynamic exception; - DeleteEmailRuleFailure(this.exception); - - @override - List get props => [exception]; + DeleteEmailRuleFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/delete_identity_state.dart b/lib/features/manage_account/domain/state/delete_identity_state.dart index e5a098fa5b..1d6cb7d4de 100644 --- a/lib/features/manage_account/domain/state/delete_identity_state.dart +++ b/lib/features/manage_account/domain/state/delete_identity_state.dart @@ -11,10 +11,6 @@ class DeleteIdentitySuccess extends UIState { } class DeleteIdentityFailure extends FeatureFailure { - final dynamic exception; - DeleteIdentityFailure(this.exception); - - @override - List get props => [exception]; + DeleteIdentityFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/delete_recipient_in_forwarding_state.dart b/lib/features/manage_account/domain/state/delete_recipient_in_forwarding_state.dart index 5040b89a49..e2506d4941 100644 --- a/lib/features/manage_account/domain/state/delete_recipient_in_forwarding_state.dart +++ b/lib/features/manage_account/domain/state/delete_recipient_in_forwarding_state.dart @@ -2,13 +2,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:forward/forward/tmail_forward.dart'; -class StartDeleteRecipientInForwarding extends UIState { - - StartDeleteRecipientInForwarding(); - - @override - List get props => []; -} +class StartDeleteRecipientInForwarding extends UIState {} class DeleteRecipientInForwardingSuccess extends UIState { final TMailForward forward; @@ -20,10 +14,6 @@ class DeleteRecipientInForwardingSuccess extends UIState { } class DeleteRecipientInForwardingFailure extends FeatureFailure { - final dynamic exception; - - DeleteRecipientInForwardingFailure(this.exception); - @override - List get props => [exception]; + DeleteRecipientInForwardingFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/edit_default_identity_state.dart b/lib/features/manage_account/domain/state/edit_default_identity_state.dart index 53537d6e3d..99e61284c5 100644 --- a/lib/features/manage_account/domain/state/edit_default_identity_state.dart +++ b/lib/features/manage_account/domain/state/edit_default_identity_state.dart @@ -1,21 +1,12 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/edit_identity_state.dart'; class EditDefaultIdentityLoading extends UIState {} -class EditDefaultIdentitySuccess extends EditIdentitySuccess { - - EditDefaultIdentitySuccess(); - - @override - List get props => []; -} +class EditDefaultIdentitySuccess extends EditIdentitySuccess {} class EditDefaultIdentityFailure extends FeatureFailure { - final dynamic exception; - - EditDefaultIdentityFailure(this.exception); - @override - List get props => [exception]; + EditDefaultIdentityFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/edit_email_rule_filter_state.dart b/lib/features/manage_account/domain/state/edit_email_rule_filter_state.dart index ce4a8095dd..daee520a48 100644 --- a/lib/features/manage_account/domain/state/edit_email_rule_filter_state.dart +++ b/lib/features/manage_account/domain/state/edit_email_rule_filter_state.dart @@ -13,10 +13,6 @@ class EditEmailRuleFilterSuccess extends UIState { } class EditEmailRuleFilterFailure extends FeatureFailure { - final dynamic exception; - EditEmailRuleFilterFailure(this.exception); - - @override - List get props => [exception]; + EditEmailRuleFilterFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/edit_identity_state.dart b/lib/features/manage_account/domain/state/edit_identity_state.dart index 1be2e1055e..8d403ad4c2 100644 --- a/lib/features/manage_account/domain/state/edit_identity_state.dart +++ b/lib/features/manage_account/domain/state/edit_identity_state.dart @@ -1,20 +1,11 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; class EditIdentityLoading extends UIState {} -class EditIdentitySuccess extends UIState { - - EditIdentitySuccess(); - - @override - List get props => []; -} +class EditIdentitySuccess extends UIState {} class EditIdentityFailure extends FeatureFailure { - final dynamic exception; - - EditIdentityFailure(this.exception); - @override - List get props => [exception]; + EditIdentityFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/edit_local_copy_in_forwarding_state.dart b/lib/features/manage_account/domain/state/edit_local_copy_in_forwarding_state.dart index 4602fe028e..fc0da82a8f 100644 --- a/lib/features/manage_account/domain/state/edit_local_copy_in_forwarding_state.dart +++ b/lib/features/manage_account/domain/state/edit_local_copy_in_forwarding_state.dart @@ -12,10 +12,6 @@ class EditLocalCopyInForwardingSuccess extends UIState { } class EditLocalCopyInForwardingFailure extends FeatureFailure { - final dynamic exception; - EditLocalCopyInForwardingFailure(this.exception); - - @override - List get props => [exception]; + EditLocalCopyInForwardingFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/get_all_identities_state.dart b/lib/features/manage_account/domain/state/get_all_identities_state.dart index 205fba488e..d35504208e 100644 --- a/lib/features/manage_account/domain/state/get_all_identities_state.dart +++ b/lib/features/manage_account/domain/state/get_all_identities_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; @@ -15,10 +16,6 @@ class GetAllIdentitiesSuccess extends UIState { } class GetAllIdentitiesFailure extends FeatureFailure { - final dynamic exception; - GetAllIdentitiesFailure(this.exception); - - @override - List get props => [exception]; + GetAllIdentitiesFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/get_all_rules_state.dart b/lib/features/manage_account/domain/state/get_all_rules_state.dart index e1320da182..d512a2d4e8 100644 --- a/lib/features/manage_account/domain/state/get_all_rules_state.dart +++ b/lib/features/manage_account/domain/state/get_all_rules_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:rule_filter/rule_filter/tmail_rule.dart'; class GetAllRulesSuccess extends UIState { @@ -11,10 +12,6 @@ class GetAllRulesSuccess extends UIState { } class GetAllRulesFailure extends FeatureFailure { - final dynamic exception; - GetAllRulesFailure(this.exception); - - @override - List get props => [exception]; + GetAllRulesFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/get_all_vacation_state.dart b/lib/features/manage_account/domain/state/get_all_vacation_state.dart index f8d5df82cd..ae02c7c65d 100644 --- a/lib/features/manage_account/domain/state/get_all_vacation_state.dart +++ b/lib/features/manage_account/domain/state/get_all_vacation_state.dart @@ -2,13 +2,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart'; -class LoadingGetAllVacation extends UIState { - - LoadingGetAllVacation(); - - @override - List get props => []; -} +class LoadingGetAllVacation extends UIState {} class GetAllVacationSuccess extends UIState { final List listVacationResponse; @@ -20,10 +14,6 @@ class GetAllVacationSuccess extends UIState { } class GetAllVacationFailure extends FeatureFailure { - final dynamic exception; - - GetAllVacationFailure(this.exception); - @override - List get props => [exception]; + GetAllVacationFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/get_forward_state.dart b/lib/features/manage_account/domain/state/get_forward_state.dart index 1102a406e3..d001619708 100644 --- a/lib/features/manage_account/domain/state/get_forward_state.dart +++ b/lib/features/manage_account/domain/state/get_forward_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:forward/forward/tmail_forward.dart'; class GetForwardSuccess extends UIState { @@ -11,10 +12,6 @@ class GetForwardSuccess extends UIState { } class GetForwardFailure extends FeatureFailure { - final dynamic exception; - GetForwardFailure(this.exception); - - @override - List get props => [exception]; + GetForwardFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/log_out_oidc_state.dart b/lib/features/manage_account/domain/state/log_out_oidc_state.dart index 4801a724a3..e2ddfde137 100644 --- a/lib/features/manage_account/domain/state/log_out_oidc_state.dart +++ b/lib/features/manage_account/domain/state/log_out_oidc_state.dart @@ -2,19 +2,9 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; -class LogoutOidcSuccess extends UIState { - - LogoutOidcSuccess(); - - @override - List get props => []; -} +class LogoutOidcSuccess extends UIState {} class LogoutOidcFailure extends FeatureFailure { - final dynamic exception; - - LogoutOidcFailure(this.exception); - @override - List get props => [exception]; + LogoutOidcFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/save_language_state.dart b/lib/features/manage_account/domain/state/save_language_state.dart index 1accaf9a17..b1ffb4f4b8 100644 --- a/lib/features/manage_account/domain/state/save_language_state.dart +++ b/lib/features/manage_account/domain/state/save_language_state.dart @@ -1,14 +1,8 @@ import 'dart:ui'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; -import 'package:core/core.dart'; - -class SavingLanguage extends UIState { - - SavingLanguage(); - - @override - List get props => []; -} +class SavingLanguage extends UIState {} class SaveLanguageSuccess extends UIState { @@ -21,10 +15,6 @@ class SaveLanguageSuccess extends UIState { } class SaveLanguageFailure extends FeatureFailure { - final dynamic exception; - - SaveLanguageFailure(this.exception); - @override - List get props => [exception]; + SaveLanguageFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/transform_html_signature_state.dart b/lib/features/manage_account/domain/state/transform_html_signature_state.dart new file mode 100644 index 0000000000..cbb7956895 --- /dev/null +++ b/lib/features/manage_account/domain/state/transform_html_signature_state.dart @@ -0,0 +1,18 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class TransformHtmlSignatureLoading extends UIState {} + +class TransformHtmlSignatureSuccess extends UIState { + final String signature; + + TransformHtmlSignatureSuccess(this.signature); + + @override + List get props => [signature]; +} + +class TransformHtmlSignatureFailure extends FeatureFailure { + + TransformHtmlSignatureFailure(exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/manage_account/domain/state/update_vacation_state.dart b/lib/features/manage_account/domain/state/update_vacation_state.dart index e2c3c6500c..faa605c763 100644 --- a/lib/features/manage_account/domain/state/update_vacation_state.dart +++ b/lib/features/manage_account/domain/state/update_vacation_state.dart @@ -2,13 +2,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart'; -class LoadingUpdateVacation extends UIState { - - LoadingUpdateVacation(); - - @override - List get props => []; -} +class LoadingUpdateVacation extends UIState {} class UpdateVacationSuccess extends UIState { final List listVacationResponse; @@ -20,10 +14,6 @@ class UpdateVacationSuccess extends UIState { } class UpdateVacationFailure extends FeatureFailure { - final dynamic exception; - - UpdateVacationFailure(this.exception); - @override - List get props => [exception]; + UpdateVacationFailure(exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/manage_account/domain/usecases/create_new_default_identity_interactor.dart b/lib/features/manage_account/domain/usecases/create_new_default_identity_interactor.dart index a030a7d5f5..a4a5101ccb 100644 --- a/lib/features/manage_account/domain/usecases/create_new_default_identity_interactor.dart +++ b/lib/features/manage_account/domain/usecases/create_new_default_identity_interactor.dart @@ -1,9 +1,11 @@ import 'dart:core'; -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_default_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_identity_request.dart'; @@ -19,26 +21,31 @@ class CreateNewDefaultIdentityInteractor { this._identityRepository, this._identityUtils); - Stream> execute(AccountId accountId, CreateNewIdentityRequest identityRequest) async* { + Stream> execute( + Session session, + AccountId accountId, + CreateNewIdentityRequest identityRequest + ) async* { try { yield Right(CreateNewDefaultIdentityLoading()); - final listDefaultIdentities = await _getDefaultIdentities(accountId); + final listDefaultIdentities = await _getDefaultIdentities(session, accountId); final defaultRequest = _createNewIdentityDefault(identityRequest, listDefaultIdentities); - final newIdentity = await _identityRepository.createNewIdentity(accountId, defaultRequest); + final newIdentity = await _identityRepository.createNewIdentity(session, accountId, defaultRequest); yield Right(CreateNewDefaultIdentitySuccess(newIdentity)); } catch (exception) { yield Left(CreateNewDefaultIdentityFailure(exception)); } } - Future?> _getDefaultIdentities(AccountId accountId) async { + Future?> _getDefaultIdentities(Session session, AccountId accountId) async { final listIdentities = await _identityRepository - .getAllIdentities( - accountId, - properties: Properties({'sortOrder', 'mayDelete'}) - ); + .getAllIdentities( + session, + accountId, + properties: Properties({'sortOrder', 'mayDelete'}) + ); listIdentities.identities?.removeWhere(_isIdentityUnDeletable); return _identityUtils.getSmallestOrderedIdentity(listIdentities.identities); } diff --git a/lib/features/manage_account/domain/usecases/create_new_identity_interactor.dart b/lib/features/manage_account/domain/usecases/create_new_identity_interactor.dart index 68c3569cc4..8f98030900 100644 --- a/lib/features/manage_account/domain/usecases/create_new_identity_interactor.dart +++ b/lib/features/manage_account/domain/usecases/create_new_identity_interactor.dart @@ -3,6 +3,7 @@ import 'dart:core'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/repository/identity_repository.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/create_new_identity_state.dart'; @@ -12,10 +13,14 @@ class CreateNewIdentityInteractor { CreateNewIdentityInteractor(this._identityRepository); - Stream> execute(AccountId accountId, CreateNewIdentityRequest identityRequest) async* { + Stream> execute( + Session session, + AccountId accountId, + CreateNewIdentityRequest identityRequest + ) async* { try { yield Right(CreateNewIdentityLoading()); - final newIdentity = await _identityRepository.createNewIdentity(accountId, identityRequest); + final newIdentity = await _identityRepository.createNewIdentity(session, accountId, identityRequest); yield Right(CreateNewIdentitySuccess(newIdentity)); } catch (exception) { yield Left(CreateNewIdentityFailure(exception)); diff --git a/lib/features/manage_account/domain/usecases/delete_identity_interactor.dart b/lib/features/manage_account/domain/usecases/delete_identity_interactor.dart index 1d5ff9f6a7..cf2833df24 100644 --- a/lib/features/manage_account/domain/usecases/delete_identity_interactor.dart +++ b/lib/features/manage_account/domain/usecases/delete_identity_interactor.dart @@ -1,8 +1,10 @@ import 'dart:core'; -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:tmail_ui_user/features/manage_account/domain/repository/identity_repository.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/delete_identity_state.dart'; @@ -12,10 +14,14 @@ class DeleteIdentityInteractor { DeleteIdentityInteractor(this._identityRepository); - Stream> execute(AccountId accountId, IdentityId identityId) async* { + Stream> execute( + Session session, + AccountId accountId, + IdentityId identityId + ) async* { try { yield Right(DeleteIdentityLoading()); - final result = await _identityRepository.deleteIdentity(accountId, identityId); + final result = await _identityRepository.deleteIdentity(session, accountId, identityId); yield result ? Right(DeleteIdentitySuccess()) : Left(DeleteIdentityFailure(null)); } catch (exception) { yield Left(DeleteIdentityFailure(exception)); diff --git a/lib/features/manage_account/domain/usecases/edit_default_identity_interactor.dart b/lib/features/manage_account/domain/usecases/edit_default_identity_interactor.dart index aea9fe7d7f..97c4c26e41 100644 --- a/lib/features/manage_account/domain/usecases/edit_default_identity_interactor.dart +++ b/lib/features/manage_account/domain/usecases/edit_default_identity_interactor.dart @@ -4,6 +4,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/edit_default_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/edit_identity_request.dart'; @@ -21,13 +22,14 @@ class EditDefaultIdentityInteractor { ); Stream> execute( - AccountId accountId, - EditIdentityRequest editIdentityRequest + Session session, + AccountId accountId, + EditIdentityRequest editIdentityRequest ) async* { try { yield Right(EditDefaultIdentityLoading()); - final defaultIdentities = await _getDefaultIdentities(accountId); + final defaultIdentities = await _getDefaultIdentities(session, accountId); _removeEditIdentityFromDefaultIdentities(defaultIdentities, editIdentityRequest.identityId); final editDefaultRequest = EditDefaultIdentityRequest( @@ -38,19 +40,20 @@ class EditDefaultIdentityInteractor { ?.map((identity) => identity.id!) .toList()); - final result = await _identityRepository.editIdentity(accountId, editDefaultRequest); + final result = await _identityRepository.editIdentity(session, accountId, editDefaultRequest); yield result ? Right(EditDefaultIdentitySuccess()) : Left(EditDefaultIdentityFailure(null)); } catch (exception) { yield Left(EditDefaultIdentityFailure(exception)); } } - Future?> _getDefaultIdentities(AccountId accountId) async { + Future?> _getDefaultIdentities(Session session, AccountId accountId) async { final listIdentities = await _identityRepository - .getAllIdentities( - accountId, - properties: Properties({'sortOrder'}) - ); + .getAllIdentities( + session, + accountId, + properties: Properties({'sortOrder'}) + ); return _identityUtils .getSmallestOrderedIdentity(listIdentities.identities) ?.toList(); diff --git a/lib/features/manage_account/domain/usecases/edit_identity_interactor.dart b/lib/features/manage_account/domain/usecases/edit_identity_interactor.dart index ebf4333e41..494fc87f09 100644 --- a/lib/features/manage_account/domain/usecases/edit_identity_interactor.dart +++ b/lib/features/manage_account/domain/usecases/edit_identity_interactor.dart @@ -1,8 +1,10 @@ import 'dart:core'; -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/edit_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/repository/identity_repository.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/edit_identity_state.dart'; @@ -13,12 +15,13 @@ class EditIdentityInteractor { EditIdentityInteractor(this._identityRepository); Stream> execute( - AccountId accountId, - EditIdentityRequest editIdentityRequest + Session session, + AccountId accountId, + EditIdentityRequest editIdentityRequest ) async* { try { yield Right(EditIdentityLoading()); - final result = await _identityRepository.editIdentity(accountId, editIdentityRequest); + final result = await _identityRepository.editIdentity(session, accountId, editIdentityRequest); yield result ? Right(EditIdentitySuccess()) : Left(EditIdentityFailure(null)); } catch (exception) { yield Left(EditIdentityFailure(exception)); diff --git a/lib/features/manage_account/domain/usecases/get_all_identities_interactor.dart b/lib/features/manage_account/domain/usecases/get_all_identities_interactor.dart index ea18b65a8f..ba19dfb264 100644 --- a/lib/features/manage_account/domain/usecases/get_all_identities_interactor.dart +++ b/lib/features/manage_account/domain/usecases/get_all_identities_interactor.dart @@ -1,12 +1,16 @@ import 'dart:core'; -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/manage_account/domain/repository/identity_repository.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/utils/identity_utils.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; class GetAllIdentitiesInteractor { final IdentityRepository _identityRepository; @@ -14,11 +18,12 @@ class GetAllIdentitiesInteractor { GetAllIdentitiesInteractor(this._identityRepository, this._identityUtils); - Stream> execute(AccountId accountId, {Properties? properties}) async* { + Stream> execute(Session session, AccountId accountId, {Properties? properties}) async* { try { yield Right(GetAllIdentitiesLoading()); - final identitiesResponse = await _identityRepository.getAllIdentities(accountId, properties: properties); - if (identitiesResponse.identities != null) { + final identitiesResponse = await _identityRepository.getAllIdentities(session, accountId, properties: properties); + final sortOrderIsSupported = [CapabilityIdentifier.jamesSortOrder].isSupported(session, accountId); + if (sortOrderIsSupported && identitiesResponse.identities != null) { _identityUtils.sortListIdentities(identitiesResponse.identities!); } yield Right(GetAllIdentitiesSuccess(identitiesResponse.identities, identitiesResponse.state)); diff --git a/lib/features/manage_account/domain/usecases/log_out_oidc_interactor.dart b/lib/features/manage_account/domain/usecases/log_out_oidc_interactor.dart index 9c9c413e95..48a6a8f2aa 100644 --- a/lib/features/manage_account/domain/usecases/log_out_oidc_interactor.dart +++ b/lib/features/manage_account/domain/usecases/log_out_oidc_interactor.dart @@ -1,10 +1,10 @@ - import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:model/account/authentication_type.dart'; import 'package:model/oidc/oidc_configuration.dart'; +import 'package:model/oidc/response/oidc_discovery_response.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; @@ -12,7 +12,6 @@ import 'package:tmail_ui_user/features/login/domain/repository/authentication_oi import 'package:tmail_ui_user/features/manage_account/domain/state/log_out_oidc_state.dart'; class LogoutOidcInteractor { - final AccountRepository _accountRepository; final AuthenticationOIDCRepository _authenticationOIDCRepository; @@ -23,16 +22,18 @@ class LogoutOidcInteractor { final currentAccount = await _accountRepository.getCurrentAccount(); log('LogoutOidcInteractor::execute(): currentAccount: $currentAccount'); if (currentAccount.authenticationType == AuthenticationType.oidc) { - final result = await Future.wait([ - _authenticationOIDCRepository.getStoredTokenOIDC(currentAccount.id), - _authenticationOIDCRepository.getStoredOidcConfiguration() - ]).then((result) async { - final tokenOidc = result.first as TokenOIDC; - final oidcConfig = result.last as OIDCConfiguration; - log('LogoutOidcInteractor::execute(): tokenOidc: ${tokenOidc.tokenId.uuid}'); - log('LogoutOidcInteractor::execute(): oidcConfig: $oidcConfig'); - return await _authenticationOIDCRepository.logout(tokenOidc.tokenId, oidcConfig); - }); + final result = await _authenticationOIDCRepository.getStoredOidcConfiguration() + .then((oidcConfig) => Future.wait([ + Future.value(oidcConfig), + _authenticationOIDCRepository.getStoredTokenOIDC(currentAccount.id), + _authenticationOIDCRepository.discoverOIDC(oidcConfig) + ])) + .then((oidcParameters) async { + final oidcConfig = oidcParameters[0] as OIDCConfiguration; + final tokenOIDC = oidcParameters[1] as TokenOIDC; + final oidcDiscoveryResponse = oidcParameters[2] as OIDCDiscoveryResponse; + return await _authenticationOIDCRepository.logout(tokenOIDC.tokenId, oidcConfig, oidcDiscoveryResponse); + }); log('LogoutOidcInteractor::execute(): statusSuccess: $result'); if (result) { yield Right(LogoutOidcSuccess()); @@ -40,11 +41,12 @@ class LogoutOidcInteractor { yield Left(LogoutOidcFailure(null)); } } else { - yield Left(LogoutOidcFailure(NotFoundAuthenticatedAccountException())); + yield Left( + LogoutOidcFailure(NotFoundAuthenticatedAccountException())); } } catch (e) { log('LogoutOidcInteractor::execute(): EXCEPTION: $e'); yield Left(LogoutOidcFailure(e)); } } -} \ No newline at end of file +} diff --git a/lib/features/manage_account/domain/usecases/transform_html_signature_interactor.dart b/lib/features/manage_account/domain/usecases/transform_html_signature_interactor.dart new file mode 100644 index 0000000000..b4e2f80cbf --- /dev/null +++ b/lib/features/manage_account/domain/usecases/transform_html_signature_interactor.dart @@ -0,0 +1,24 @@ +import 'dart:core'; + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/repository/identity_repository.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/edit_identity_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/transform_html_signature_state.dart'; + +class TransformHtmlSignatureInteractor { + final IdentityRepository _identityRepository; + + TransformHtmlSignatureInteractor(this._identityRepository); + + Stream> execute(String signature) async* { + try { + yield Right(TransformHtmlSignatureLoading()); + final signatureUnescape = await _identityRepository.transformHtmlSignature(signature); + yield Right(TransformHtmlSignatureSuccess(signatureUnescape)); + } catch (exception) { + yield Left(EditIdentityFailure(exception)); + } + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/base/setting_detail_view_builder.dart b/lib/features/manage_account/presentation/base/setting_detail_view_builder.dart new file mode 100644 index 0000000000..56d6770542 --- /dev/null +++ b/lib/features/manage_account/presentation/base/setting_detail_view_builder.dart @@ -0,0 +1,39 @@ + +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; + +class SettingDetailViewBuilder extends StatelessWidget { + + final ResponsiveUtils responsiveUtils; + final Widget child; + final EdgeInsets? padding; + final VoidCallback? onTapGestureDetector; + + const SettingDetailViewBuilder({ + super.key, + required this.responsiveUtils, + required this.child, + this.padding, + this.onTapGestureDetector + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: SettingsUtils.getBackgroundColor(context, responsiveUtils), + body: GestureDetector( + onTap: onTapGestureDetector, + child: Container( + width: double.infinity, + height: double.infinity, + color: SettingsUtils.getContentBackgroundColor(context, responsiveUtils), + decoration: SettingsUtils.getBoxDecorationForContent(context, responsiveUtils), + margin: SettingsUtils.getMarginSettingDetailsView(context, responsiveUtils), + padding: padding, + child: child, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/email_rules/email_rules_controller.dart b/lib/features/manage_account/presentation/email_rules/email_rules_controller.dart index 5dbf62b0a5..5e289ebb96 100644 --- a/lib/features/manage_account/presentation/email_rules/email_rules_controller.dart +++ b/lib/features/manage_account/presentation/email_rules/email_rules_controller.dart @@ -1,11 +1,12 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart'; import 'package:core/presentation/views/dialog/confirmation_dialog_builder.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -29,6 +30,7 @@ import 'package:tmail_ui_user/features/manage_account/presentation/manage_accoun import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/creator_action_type.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; @@ -46,25 +48,6 @@ class EmailRulesController extends BaseController { final listEmailRule = [].obs; - EmailRulesController(); - - @override - void onDone() { - viewState.value.fold((failure) {}, (success) { - if (success is GetAllRulesSuccess) { - if (success.rules?.isNotEmpty == true) { - listEmailRule.addAll(success.rules!); - } - } else if (success is DeleteEmailRuleSuccess) { - _handleDeleteEmailRuleSuccess(success); - } else if (success is CreateNewRuleFilterSuccess) { - _createNewRuleFilterSuccess(success); - } else if (success is EditEmailRuleFilterSuccess) { - _editEmailRuleFilterSuccess(success); - } - }); - } - @override void onInit() { super.onInit(); @@ -84,29 +67,34 @@ class EmailRulesController extends BaseController { super.onReady(); } + @override + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetAllRulesSuccess) { + if (success.rules?.isNotEmpty == true) { + listEmailRule.addAll(success.rules!); + } + } else if (success is DeleteEmailRuleSuccess) { + _handleDeleteEmailRuleSuccess(success); + } else if (success is CreateNewRuleFilterSuccess) { + _createNewRuleFilterSuccess(success); + } else if (success is EditEmailRuleFilterSuccess) { + _editEmailRuleFilterSuccess(success); + } + } + void goToCreateNewRule(BuildContext context) async { final accountId = _accountDashBoardController.accountId.value; - final session = _accountDashBoardController.sessionCurrent.value; + final session = _accountDashBoardController.sessionCurrent; if (accountId != null && session != null) { final arguments = RulesFilterCreatorArguments(accountId, session); - if (BuildUtils.isWeb) { - showDialogRuleFilterCreator( - context: context, - arguments: arguments, - onCreatedRuleFilter: (arguments) { - if (arguments is CreateNewEmailRuleFilterRequest) { - _createNewRuleFilterAction(accountId, arguments); - } - }); - } else { - final newRuleFilterRequest = await push( - AppRoutes.rulesFilterCreator, - arguments: arguments); + final newRuleFilterRequest = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.rulesFilterCreator, arguments: arguments) + : await push(AppRoutes.rulesFilterCreator, arguments: arguments); - if (newRuleFilterRequest is CreateNewEmailRuleFilterRequest) { - _createNewRuleFilterAction(accountId, newRuleFilterRequest); - } + if (newRuleFilterRequest is CreateNewEmailRuleFilterRequest) { + _createNewRuleFilterAction(accountId, newRuleFilterRequest); } } } @@ -123,10 +111,9 @@ class EmailRulesController extends BaseController { void _createNewRuleFilterSuccess(CreateNewRuleFilterSuccess success) { if (success.newListRules.isNotEmpty == true) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - message: AppLocalizations.of(currentContext!).newFilterWasCreated, - icon: _imagePaths.icSelected); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).newFilterWasCreated); } listEmailRule.value = success.newListRules; listEmailRule.refresh(); @@ -135,7 +122,7 @@ class EmailRulesController extends BaseController { void editEmailRule(BuildContext context, TMailRule rule) async { final accountId = _accountDashBoardController.accountId.value; - final session = _accountDashBoardController.sessionCurrent.value; + final session = _accountDashBoardController.sessionCurrent; if (accountId != null && session != null) { final arguments = RulesFilterCreatorArguments( accountId, @@ -143,23 +130,12 @@ class EmailRulesController extends BaseController { actionType: CreatorActionType.edit, tMailRule: rule); - if (BuildUtils.isWeb) { - showDialogRuleFilterCreator( - context: context, - arguments: arguments, - onCreatedRuleFilter: (arguments) { - if (arguments is EditEmailRuleFilterRequest) { - _editEmailRuleFilterAction(accountId, arguments); - } - }); - } else { - final newRuleFilterRequest = await push( - AppRoutes.rulesFilterCreator, - arguments: arguments); + final newRuleFilterRequest = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.rulesFilterCreator, arguments: arguments) + : await push(AppRoutes.rulesFilterCreator, arguments: arguments); - if (newRuleFilterRequest is EditEmailRuleFilterRequest) { - _editEmailRuleFilterAction(accountId, newRuleFilterRequest); - } + if (newRuleFilterRequest is EditEmailRuleFilterRequest) { + _editEmailRuleFilterAction(accountId, newRuleFilterRequest); } } } @@ -176,10 +152,9 @@ class EmailRulesController extends BaseController { void _editEmailRuleFilterSuccess(EditEmailRuleFilterSuccess success) { if (success.listRulesUpdated.isNotEmpty == true) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - message: AppLocalizations.of(currentContext!).yourFilterHasBeenUpdated, - icon: _imagePaths.icSelected); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).yourFilterHasBeenUpdated); } listEmailRule.value = success.listRulesUpdated; listEmailRule.refresh(); @@ -239,11 +214,9 @@ class EmailRulesController extends BaseController { void _handleDeleteEmailRuleSuccess(DeleteEmailRuleSuccess success) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( + _appToast.showToastSuccessMessage( currentOverlayContext!, - message: AppLocalizations.of(currentContext!).toastMessageDeleteEmailRuleSuccessfully, - icon: _imagePaths.icSelected, - ); + AppLocalizations.of(currentContext!).toastMessageDeleteEmailRuleSuccessfully); } if (success.rules?.isNotEmpty == true) { @@ -271,8 +244,9 @@ class EmailRulesController extends BaseController { Widget _deleteEmailRuleActionTile(BuildContext context, TMailRule rule) { return (EmailRuleBottomSheetActionTileBuilder( const Key('delete_emailRule_action'), - SvgPicture.asset(_imagePaths.icDeleteComposer, - color: AppColor.colorActionDeleteConfirmDialog), + SvgPicture.asset( + _imagePaths.icDeleteComposer, + colorFilter: AppColor.colorActionDeleteConfirmDialog.asFilter()), AppLocalizations.of(context).deleteRule, rule, iconLeftPadding: const EdgeInsets.only(left: 12, right: 16), diff --git a/lib/features/manage_account/presentation/email_rules/widgets/email_rule_bottom_sheet_action_tile_builder.dart b/lib/features/manage_account/presentation/email_rules/widgets/email_rule_bottom_sheet_action_tile_builder.dart index 4ec29bd898..2d83022591 100644 --- a/lib/features/manage_account/presentation/email_rules/widgets/email_rule_bottom_sheet_action_tile_builder.dart +++ b/lib/features/manage_account/presentation/email_rules/widgets/email_rule_bottom_sheet_action_tile_builder.dart @@ -30,7 +30,7 @@ class EmailRuleBottomSheetActionTileBuilder return Container( color: bgColor ?? Colors.white, child: MouseRegion( - cursor: BuildUtils.isWeb + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( diff --git a/lib/features/manage_account/presentation/email_rules/widgets/email_rule_item_widget.dart b/lib/features/manage_account/presentation/email_rules/widgets/email_rule_item_widget.dart index ae2ab56583..ce657ae35f 100644 --- a/lib/features/manage_account/presentation/email_rules/widgets/email_rule_item_widget.dart +++ b/lib/features/manage_account/presentation/email_rules/widgets/email_rule_item_widget.dart @@ -39,6 +39,7 @@ class EmailRulesItemWidget extends StatelessWidget { icon: SvgPicture.asset( _imagePaths.icEditRule, fit: BoxFit.fill, + colorFilter: AppColor.primaryColor.asFilter(), ), onTap: () { _emailRuleController.editEmailRule(context, rule); diff --git a/lib/features/manage_account/presentation/email_rules/widgets/email_rules_header_widget.dart b/lib/features/manage_account/presentation/email_rules/widgets/email_rules_header_widget.dart index 3b4231725e..9c2670a99d 100644 --- a/lib/features/manage_account/presentation/email_rules/widgets/email_rules_header_widget.dart +++ b/lib/features/manage_account/presentation/email_rules/widgets/email_rules_header_widget.dart @@ -1,7 +1,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/presentation/views/button/button_builder.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:flutter/material.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -46,50 +46,45 @@ class EmailRulesHeaderWidget extends StatelessWidget { Widget _buildButtonAddNewRule(BuildContext context) { if (!responsiveUtils.isMobile(context)) { - return (ButtonBuilder(imagePaths.icAddNewRules) - ..key(const Key('button_new_rule')) - ..decoration(BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: AppColor.colorTextButton)) - ..paddingIcon(const EdgeInsets.only(right: 8)) - ..iconColor(Colors.white) - ..maxWidth(130) - ..size(20) - ..radiusSplash(10) - ..padding(const EdgeInsets.symmetric(vertical: 12)) - ..textStyle(const TextStyle( + return Row( + children: [ + TMailButtonWidget( + key: const Key('new_rule_button'), + text: AppLocalizations.of(context).addNewRule, + icon: imagePaths.icAddNewRules, + borderRadius: 10, + backgroundColor: AppColor.colorTextButton, + iconColor: Colors.white, + minWidth: 130, + padding: const EdgeInsets.symmetric(vertical: 12,horizontal: 8), + iconSize: 20, + textStyle: const TextStyle( fontSize: 17, color: Colors.white, fontWeight: FontWeight.w500, - )) - ..onPressActionClick(() => createRule.call()) - ..text( - AppLocalizations.of(context).addNewRule, - isVertical: false, - )) - .build(); + ), + onTapActionCallback: createRule, + ), + const Spacer(), + ], + ); } else { - return (ButtonBuilder(imagePaths.icAddNewRules) - ..key(const Key('button_new_rule')) - ..decoration(BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: AppColor.colorTextButton)) - ..paddingIcon(const EdgeInsets.only(right: 8)) - ..iconColor(Colors.white) - ..size(20) - ..radiusSplash(10) - ..padding(const EdgeInsets.symmetric(vertical: 12)) - ..textStyle(const TextStyle( - fontSize: 17, - color: Colors.white, - fontWeight: FontWeight.w500, - )) - ..onPressActionClick(() => createRule.call()) - ..text( - AppLocalizations.of(context).addNewRule, - isVertical: false, - )) - .build(); + return TMailButtonWidget( + key: const Key('new_rule_button'), + text: AppLocalizations.of(context).addNewRule, + icon: imagePaths.icAddNewRules, + borderRadius: 10, + backgroundColor: AppColor.colorTextButton, + iconColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + iconSize: 20, + textStyle: const TextStyle( + fontSize: 17, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + onTapActionCallback: createRule, + ); } } } diff --git a/lib/features/manage_account/presentation/email_rules/widgets/list_email_rules_widget.dart b/lib/features/manage_account/presentation/email_rules/widgets/list_email_rules_widget.dart index 72cb839b69..e3ec79fdfc 100644 --- a/lib/features/manage_account/presentation/email_rules/widgets/list_email_rules_widget.dart +++ b/lib/features/manage_account/presentation/email_rules/widgets/list_email_rules_widget.dart @@ -47,9 +47,7 @@ class ListEmailRulesWidget extends GetWidget { ), const Divider( color: AppColor.lineItemListColor, - height: 1, - thickness: 0.2, - ), + height: 1), Obx(() { log('ListEmailRulesWidget::build(): ${controller.listEmailRule}'); return ListView.separated( @@ -67,9 +65,7 @@ class ListEmailRulesWidget extends GetWidget { if (controller.listEmailRule.isNotEmpty) { return const Divider( color: AppColor.lineItemListColor, - height: 1, - thickness: 0.2, - ); + height: 1); } else { return const SizedBox.shrink(); } diff --git a/lib/features/manage_account/presentation/extensions/identity_extension.dart b/lib/features/manage_account/presentation/extensions/identity_extension.dart new file mode 100644 index 0000000000..b06486910a --- /dev/null +++ b/lib/features/manage_account/presentation/extensions/identity_extension.dart @@ -0,0 +1,15 @@ + +import 'package:jmap_dart_client/jmap/identities/identity.dart'; + +extension IdentityExtension on Identity { + + String get signatureAsString { + if (htmlSignature?.value.isNotEmpty == true) { + return htmlSignature!.value; + } else if (textSignature?.value.isNotEmpty == true) { + return textSignature!.value; + } else { + return ''; + } + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/forward/controller/forward_recipient_controller.dart b/lib/features/manage_account/presentation/forward/controller/forward_recipient_controller.dart index 8dc3dd7b7d..3681f7c778 100644 --- a/lib/features/manage_account/presentation/forward/controller/forward_recipient_controller.dart +++ b/lib/features/manage_account/presentation/forward/controller/forward_recipient_controller.dart @@ -81,10 +81,6 @@ class ForwardRecipientController { } } - void updateListRecipient(List listEmailAddress) { - listRecipients.value = listEmailAddress; - } - void clearAll() { inputRecipientController.clear(); listRecipients.clear(); diff --git a/lib/features/manage_account/presentation/forward/forward_controller.dart b/lib/features/manage_account/presentation/forward/forward_controller.dart index b1c2e65ced..5f6bea7d90 100644 --- a/lib/features/manage_account/presentation/forward/forward_controller.dart +++ b/lib/features/manage_account/presentation/forward/forward_controller.dart @@ -1,6 +1,9 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/presentation/utils/keyboard_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -48,12 +51,11 @@ class ForwardController extends BaseController { bool get currentForwardLocalCopyState => currentForward.value?.localCopy ?? false; late ForwardRecipientController recipientController; - late Worker dashboardActionWorker; ForwardController() { recipientController = ForwardRecipientController( accountId: accountDashBoardController.accountId.value, - session: accountDashBoardController.sessionCurrent.value); + session: accountDashBoardController.sessionCurrent); } @override @@ -73,38 +75,38 @@ class ForwardController extends BaseController { @override void onClose() { recipientController.onClose(); - unregisterListenerWorker(); super.onClose(); } - @override - void onDone() { - viewState.value.fold( - (failure) { - if (failure is DeleteRecipientInForwardingFailure) { - cancelSelectionMode(); - } - }, - (success) { - if (success is GetForwardSuccess) { - currentForward.value = success.forward; - listRecipientForward.value = currentForward.value!.listRecipientForward; - } else if (success is DeleteRecipientInForwardingSuccess) { - _handleDeleteRecipientSuccess(success); - } else if (success is AddRecipientsInForwardingSuccess) { - _handleAddRecipientsSuccess(success); - } else if (success is EditLocalCopyInForwardingSuccess) { - _handleEditLocalCopySuccess(success); - } - }); - } - @override void onReady() { _getForward(); super.onReady(); } + @override + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetForwardSuccess) { + currentForward.value = success.forward; + listRecipientForward.value = currentForward.value!.listRecipientForward; + } else if (success is DeleteRecipientInForwardingSuccess) { + _handleDeleteRecipientSuccess(success); + } else if (success is AddRecipientsInForwardingSuccess) { + _handleAddRecipientsSuccess(success); + } else if (success is EditLocalCopyInForwardingSuccess) { + _handleEditLocalCopySuccess(success); + } + } + + @override + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is DeleteRecipientInForwardingFailure) { + cancelSelectionMode(); + } + } + void _getForward() { if (_getForwardInteractor != null) { consumeState(_getForwardInteractor!.execute(accountDashBoardController.accountId.value!)); @@ -151,11 +153,9 @@ class ForwardController extends BaseController { void _handleDeleteRecipientSuccess(DeleteRecipientInForwardingSuccess success) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( + _appToast.showToastSuccessMessage( currentOverlayContext!, - message: AppLocalizations.of(currentContext!).toastMessageDeleteRecipientSuccessfully, - icon: _imagePaths.icSelected, - ); + AppLocalizations.of(currentContext!).toastMessageDeleteRecipientSuccessfully); } currentForward.value = success.forward; @@ -234,7 +234,7 @@ class ForwardController extends BaseController { } void addRecipientAction(BuildContext context, List listRecipientsSelected) { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); final accountId = accountDashBoardController.accountId.value; if (accountId != null) { @@ -255,11 +255,9 @@ class ForwardController extends BaseController { void _handleAddRecipientsSuccess(AddRecipientsInForwardingSuccess success) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( + _appToast.showToastSuccessMessage( currentOverlayContext!, - message: AppLocalizations.of(currentContext!).toastMessageAddRecipientsSuccessfully, - icon: _imagePaths.icSelected, - ); + AppLocalizations.of(currentContext!).toastMessageAddRecipientsSuccessfully); } currentForward.value = success.forward; @@ -283,13 +281,11 @@ class ForwardController extends BaseController { void _handleEditLocalCopySuccess(EditLocalCopyInForwardingSuccess success) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( + _appToast.showToastSuccessMessage( currentOverlayContext!, - message: success.forward.localCopy ? - AppLocalizations.of(currentContext!).toastMessageLocalCopyEnable : - AppLocalizations.of(currentContext!).toastMessageLocalCopyDisable, - icon: _imagePaths.icSelected, - ); + success.forward.localCopy + ? AppLocalizations.of(currentContext!).toastMessageLocalCopyEnable + : AppLocalizations.of(currentContext!).toastMessageLocalCopyDisable); } currentForward.value = success.forward; @@ -297,7 +293,7 @@ class ForwardController extends BaseController { } void registerListenerWorker() { - dashboardActionWorker = ever( + ever( accountDashBoardController.dashboardSettingAction, (action) { if (action is ClearAllInputForwarding) { @@ -308,7 +304,15 @@ class ForwardController extends BaseController { ); } - void unregisterListenerWorker() { - dashboardActionWorker.dispose(); + void handleExceptionCallback(BuildContext context, bool isListEmailEmpty) { + if (isListEmailEmpty) { + _appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).emptyListEmailForward); + } else { + _appToast.showToastErrorMessage( + context, + AppLocalizations.of(context).incorrectEmailFormat); + } } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/forward/forward_view.dart b/lib/features/manage_account/presentation/forward/forward_view.dart index e02ec66108..4b18f12455 100644 --- a/lib/features/manage_account/presentation/forward/forward_view.dart +++ b/lib/features/manage_account/presentation/forward/forward_view.dart @@ -1,7 +1,6 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/state/success.dart'; -import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:flutter/material.dart'; @@ -18,7 +17,6 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; class ForwardView extends GetWidget with AppLoaderMixin { final _responsiveUtils = Get.find(); final _imagePaths = Get.find(); - final _appToast = Get.find(); ForwardView({Key? key}) : super(key: key); @@ -38,7 +36,7 @@ class ForwardView extends GetWidget with AppLoaderMixin { if (_responsiveUtils.isWebDesktop(context)) ...[ ForwardHeaderWidget(imagePaths: _imagePaths, responsiveUtils: _responsiveUtils), - Container(height: 1, color: AppColor.colorDividerHeaderSetting) + const Divider(height: 1, color: AppColor.colorDividerHeaderSetting) ], Expanded(child: SingleChildScrollView( physics: const ClampingScrollPhysics(), @@ -46,7 +44,10 @@ class ForwardView extends GetWidget with AppLoaderMixin { if (!_responsiveUtils.isWebDesktop(context)) _buildTitleHeader(context), _buildKeepLocalSwitchButton(context), - _buildAddRecipientsFormWidget(context), + Obx(() => controller.currentForward.value != null + ? _buildAddRecipientsFormWidget(context) + : const SizedBox.shrink() + ), _buildLoadingView(), Obx(() { if (controller.listRecipientForward.isNotEmpty) { @@ -80,36 +81,38 @@ class ForwardView extends GetWidget with AppLoaderMixin { } Widget _buildKeepLocalSwitchButton(BuildContext context) { - return Container( - color: Colors.transparent, - padding: SettingsUtils.getPaddingKeepLocalSwitchButtonForwarding(context, _responsiveUtils), - child: Row(children: [ - Obx(() { - return InkWell( - onTap: controller.handleEditLocalCopy, - child: SvgPicture.asset( - controller.currentForwardLocalCopyState - ? _imagePaths.icSwitchOn - : _imagePaths.icSwitchOff, - fit: BoxFit.fill, - width: 36, - height: 24) - ); - }), - const SizedBox(width: 16), - Expanded( - child: Text( - AppLocalizations.of(context).keepLocalCopyForwardLabel, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - color: Colors.black) - ), - ) - ]), - ); + return Obx(() { + return controller.listRecipientForward.isNotEmpty + ? Container( + color: Colors.transparent, + padding: SettingsUtils.getPaddingKeepLocalSwitchButtonForwarding(context, _responsiveUtils), + child: Row(children: [ + Padding( + padding: const EdgeInsets.only(right: 16), + child: InkWell( + onTap: controller.handleEditLocalCopy, + child: SvgPicture.asset( + controller.currentForwardLocalCopyState + ? _imagePaths.icSwitchOn + : _imagePaths.icSwitchOff, + fit: BoxFit.fill, + width: 36, + height: 24))), + Expanded( + child: Text( + AppLocalizations.of(context).keepLocalCopyForwardLabel, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Colors.black) + ), + ) + ])) + : const SizedBox(); + }); + } Widget _buildLoadingView() { @@ -132,20 +135,8 @@ class ForwardView extends GetWidget with AppLoaderMixin { onAddContactCallback: (listRecipientsSelected) { controller.addRecipientAction(context, listRecipientsSelected); }, - onExceptionCallback: () { - _appToast.showBottomToast( - context, - AppLocalizations.of(context).incorrectEmailFormat, - leadingIcon: SvgPicture.asset( - _imagePaths.icNotConnection, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), - backgroundColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - maxWidth: _responsiveUtils.getMaxWidthToast(context)); + onExceptionCallback: (isListEmailEmpty) { + controller.handleExceptionCallback(context, isListEmailEmpty); }, ); } diff --git a/lib/features/manage_account/presentation/forward/widgets/email_forward_item_widget.dart b/lib/features/manage_account/presentation/forward/widgets/email_forward_item_widget.dart index bf89f2f303..6afd7cad18 100644 --- a/lib/features/manage_account/presentation/forward/widgets/email_forward_item_widget.dart +++ b/lib/features/manage_account/presentation/forward/widgets/email_forward_item_widget.dart @@ -4,7 +4,7 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; import 'package:core/presentation/views/image/avatar_builder.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -31,7 +31,7 @@ class EmailForwardItemWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final _imagePaths = Get.find(); + final imagePaths = Get.find(); return Padding( padding: const EdgeInsets.only(top: 4), @@ -39,7 +39,7 @@ class EmailForwardItemWidget extends StatelessWidget { color: Colors.transparent, child: InkWell( onLongPress: () { - if (!BuildUtils.isWeb) { + if (PlatformInfo.isMobile) { onSelectRecipientCallback?.call(recipientForward); } }, @@ -53,7 +53,7 @@ class EmailForwardItemWidget extends StatelessWidget { recipientForward.selectMode == SelectMode.ACTIVE ? 12 : 0)) ), child: Row(children: [ - _buildAvatarIcon(_imagePaths), + _buildAvatarIcon(imagePaths), const SizedBox(width: 12), Expanded(child: Column( mainAxisSize: MainAxisSize.min, @@ -92,7 +92,7 @@ class EmailForwardItemWidget extends StatelessWidget { buildIconWeb( iconSize: 30, splashRadius: 20, - icon: SvgPicture.asset(_imagePaths.icDeleteRecipient), + icon: SvgPicture.asset(imagePaths.icDeleteRecipient), onTap: () => onDeleteRecipientCallback?.call(recipientForward) ) ]), diff --git a/lib/features/manage_account/presentation/forward/widgets/list_email_forward_widget.dart b/lib/features/manage_account/presentation/forward/widgets/list_email_forward_widget.dart index 92db800104..fd1dbbe862 100644 --- a/lib/features/manage_account/presentation/forward/widgets/list_email_forward_widget.dart +++ b/lib/features/manage_account/presentation/forward/widgets/list_email_forward_widget.dart @@ -82,6 +82,7 @@ class ListEmailForwardsWidget extends GetWidget { color: Colors.transparent, child: InkWell( customBorder: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))), + onTap: controller.selectAllRecipientForward, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), child: Text( @@ -93,7 +94,6 @@ class ListEmailForwardsWidget extends GetWidget { ) ), ), - onTap: controller.selectAllRecipientForward, ) ); } @@ -106,8 +106,8 @@ class ListEmailForwardsWidget extends GetWidget { children: [ buildIconWeb( icon: SvgPicture.asset( - _imagePaths.icCloseComposer, - color: AppColor.colorTextButton, + _imagePaths.icClose, + colorFilter: AppColor.colorTextButton.asFilter(), fit: BoxFit.fill), tooltip: AppLocalizations.of(context).cancel, onTap: controller.cancelSelectionMode diff --git a/lib/features/manage_account/presentation/language_and_region/extensions/locale_extension.dart b/lib/features/manage_account/presentation/language_and_region/extensions/locale_extension.dart index d10ee439ba..6bbd98c671 100644 --- a/lib/features/manage_account/presentation/language_and_region/extensions/locale_extension.dart +++ b/lib/features/manage_account/presentation/language_and_region/extensions/locale_extension.dart @@ -4,7 +4,7 @@ import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; extension LocaleExtension on Locale { - String getLanguageName(BuildContext context) { + String getLanguageNameByCurrentLocale(BuildContext context) { switch(languageCode) { case 'fr': return AppLocalizations.of(context).languageFrench; @@ -14,6 +14,29 @@ extension LocaleExtension on Locale { return AppLocalizations.of(context).languageVietnamese; case 'ru': return AppLocalizations.of(context).languageRussian; + case 'ar': + return AppLocalizations.of(context).languageArabic; + case 'it': + return AppLocalizations.of(context).languageItalian; + default: + return ''; + } + } + + String getSourceLanguageName() { + switch(languageCode) { + case 'fr': + return 'Français'; + case 'en': + return 'English'; + case 'vi': + return 'Tiếng Việt'; + case 'ru': + return 'Русский'; + case 'ar': + return 'عربي'; + case 'it': + return 'Italiano'; default: return ''; } diff --git a/lib/features/manage_account/presentation/language_and_region/language_and_region_controller.dart b/lib/features/manage_account/presentation/language_and_region/language_and_region_controller.dart index 75eb548d5c..0de0089de9 100644 --- a/lib/features/manage_account/presentation/language_and_region/language_and_region_controller.dart +++ b/lib/features/manage_account/presentation/language_and_region/language_and_region_controller.dart @@ -1,6 +1,8 @@ import 'dart:ui'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/save_language_state.dart'; @@ -13,6 +15,7 @@ class LanguageAndRegionController extends BaseController { final listSupportedLanguages = [].obs; final languageSelected = LocalizationService.defaultLocale.obs; + final isLanguageMenuOverlayOpen = RxBool(false); LanguageAndRegionController(this._saveLanguageInteractor); @@ -23,20 +26,18 @@ class LanguageAndRegionController extends BaseController { } @override - void onDone() { - viewState.value.fold( - (failure) => null, - (success) { - if (success is SaveLanguageSuccess) { - LocalizationService.changeLocale(success.localeStored.languageCode); - } - }); + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is SaveLanguageSuccess) { + LocalizationService.changeLocale(success.localeStored.languageCode); + } } void _setUpSupportedLanguages() { listSupportedLanguages.value = LocalizationService.supportedLocales; final currentLocale = Get.locale; + log('LanguageAndRegionController::_setUpSupportedLanguages():currentLocale: $currentLocale'); if (currentLocale != null) { languageSelected.value = currentLocale; } else { @@ -45,6 +46,7 @@ class LanguageAndRegionController extends BaseController { } void selectLanguage(Locale? selectedLocale) { + isLanguageMenuOverlayOpen.value = false; languageSelected.value = selectedLocale ?? LocalizationService.defaultLocale; _saveLanguage(languageSelected.value); } @@ -52,4 +54,8 @@ class LanguageAndRegionController extends BaseController { void _saveLanguage(Locale localeCurrent) { consumeState(_saveLanguageInteractor.execute(localeCurrent)); } + + void toggleLanguageMenuOverlay() { + isLanguageMenuOverlayOpen.toggle(); + } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/language_and_region/language_and_region_view.dart b/lib/features/manage_account/presentation/language_and_region/language_and_region_view.dart index ea50c84676..20fdcfd1a9 100644 --- a/lib/features/manage_account/presentation/language_and_region/language_and_region_view.dart +++ b/lib/features/manage_account/presentation/language_and_region/language_and_region_view.dart @@ -1,11 +1,14 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/language_and_region_controller.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/widgets/change_language_widget.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/widgets/change_language_button_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/widgets/language_and_region_header_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; class LanguageAndRegionView extends GetWidget { @@ -15,36 +18,44 @@ class LanguageAndRegionView extends GetWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: _responsiveUtils.isWebDesktop(context) - ? AppColor.colorBgDesktop - : Colors.white, - body: Container( - width: double.infinity, - margin: _responsiveUtils.isWebDesktop(context) - ? const EdgeInsets.all(24) - : EdgeInsets.symmetric(horizontal: SettingsUtils.getHorizontalPadding(context, _responsiveUtils)), - color: _responsiveUtils.isWebDesktop(context) ? null : Colors.white, - decoration: _responsiveUtils.isWebDesktop(context) - ? BoxDecoration( - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColor.colorBorderBodyThread, width: 1), - color: Colors.white) - : null, - child: ClipRRect( - borderRadius: BorderRadius.circular( - _responsiveUtils.isWebDesktop(context) ? 20 : 0), - child: Padding( - padding: EdgeInsets.only( - left: _responsiveUtils.isWebDesktop(context) ? 24 : 0, - top: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const LanguageAndRegionHeaderWidget(), - const SizedBox(height: 22), - Expanded(child: ChangeLanguageWidget()) - ] + return Portal( + child: Scaffold( + backgroundColor: _responsiveUtils.isWebDesktop(context) + ? AppColor.colorBgDesktop + : Colors.white, + body: Container( + width: double.infinity, + margin: _responsiveUtils.isWebDesktop(context) + ? const EdgeInsets.all(24) + : EdgeInsets.symmetric(horizontal: SettingsUtils.getHorizontalPadding(context, _responsiveUtils)), + color: _responsiveUtils.isWebDesktop(context) ? null : Colors.white, + decoration: _responsiveUtils.isWebDesktop(context) + ? BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColor.colorBorderBodyThread, width: 1), + color: Colors.white) + : null, + child: ClipRRect( + borderRadius: BorderRadius.circular( + _responsiveUtils.isWebDesktop(context) ? 20 : 0), + child: Padding( + padding: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) + ? _responsiveUtils.isWebDesktop(context) ? 24 : 0 + : 0, + left: AppUtils.isDirectionRTL(context) + ? 0 + : _responsiveUtils.isWebDesktop(context) ? 24 : 0, + top: 24 + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const LanguageAndRegionHeaderWidget(), + const SizedBox(height: 22), + Expanded(child: ChangeLanguageButtonWidget()) + ] + ), ), ), ), diff --git a/lib/features/manage_account/presentation/language_and_region/widgets/change_language_button_widget.dart b/lib/features/manage_account/presentation/language_and_region/widgets/change_language_button_widget.dart new file mode 100644 index 0000000000..3f29c3abc5 --- /dev/null +++ b/lib/features/manage_account/presentation/language_and_region/widgets/change_language_button_widget.dart @@ -0,0 +1,131 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/responsive/responsive_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_portal/flutter_portal.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/extensions/locale_extension.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/language_and_region_controller.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/widgets/language_menu_overlay.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class ChangeLanguageButtonWidget extends StatelessWidget { + + final _controller = Get.find(); + final _responsiveUtils = Get.find(); + final _imagePaths = Get.find(); + + ChangeLanguageButtonWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + return ResponsiveWidget( + responsiveUtils: _responsiveUtils, + mobile: Scaffold( + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitleLanguageWidget(context), + const SizedBox(height: 8), + _buildLanguageMenu(context, constraints.maxWidth) + ] + ) + ), + desktop: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTitleLanguageWidget(context), + const SizedBox(height: 8), + SizedBox( + width: constraints.maxWidth / 2, + child: _buildLanguageMenu(context, constraints.maxWidth / 2) + ), + ] + ) + ); + }); + } + + Widget _buildTitleLanguageWidget(BuildContext context) { + return Text( + AppLocalizations.of(context).language, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: AppColor.colorContentEmail + ) + ); + } + + Widget _buildLanguageMenu(BuildContext context, double maxWidth) { + return Obx(() => PortalTarget( + visible: _controller.isLanguageMenuOverlayOpen.isTrue, + portalFollower: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _controller.toggleLanguageMenuOverlay() + ), + child: PortalTarget( + anchor: const Aligned( + follower: Alignment.topRight, + target: Alignment.bottomRight, + widthFactor: 1, + backup: Aligned( + follower: Alignment.topRight, + target: Alignment.bottomRight, + widthFactor: 1, + ), + ), + portalFollower: Obx(() => LanguageRegionOverlay( + listSupportedLanguages: _controller.listSupportedLanguages, + localeSelected: _controller.languageSelected.value, + maxWidth: maxWidth, + onSelectLanguageAction: _controller.selectLanguage, + )), + visible: _controller.isLanguageMenuOverlayOpen.isTrue, + child: _buildDropDownMenuButton(context, maxWidth) + ) + )); + } + + Widget _buildDropDownMenuButton(BuildContext context, double maxWidth) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _controller.toggleLanguageMenuOverlay(), + borderRadius: const BorderRadius.all(Radius.circular(10)), + child: Container( + height: 44, + width: maxWidth, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: AppColor.colorInputBorderCreateMailbox, + width: 0.5, + ), + color: AppColor.colorItemSelected, + ), + padding: const EdgeInsetsDirectional.only(start: 12, end: 10), + child: Row(children: [ + Expanded(child: Text( + _controller.languageSelected.value.getLanguageNameByCurrentLocale(context), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black + ), + maxLines: 1, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + )), + SvgPicture.asset(_imagePaths.icDropDown) + ]), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/language_and_region/widgets/change_language_widget.dart b/lib/features/manage_account/presentation/language_and_region/widgets/change_language_widget.dart deleted file mode 100644 index 4b26260da2..0000000000 --- a/lib/features/manage_account/presentation/language_and_region/widgets/change_language_widget.dart +++ /dev/null @@ -1,59 +0,0 @@ - -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/base/widget/drop_down_button_widget.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/language_and_region_controller.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -class ChangeLanguageWidget extends StatelessWidget { - - final _controller = Get.find(); - final _responsiveUtils = Get.find(); - - ChangeLanguageWidget({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final titleLanguageWidget = Text( - AppLocalizations.of(context).language, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: AppColor.colorContentEmail)); - - final dropDownMenuSelectLanguage = Row(children: [ - Expanded(child: Obx(() => - DropDownButtonWidget( - items: _controller.listSupportedLanguages, - itemSelected: _controller.languageSelected.value, - onChanged: (newLanguage) => - _controller.selectLanguage(newLanguage), - supportSelectionIcon: true))), - ]); - - return LayoutBuilder(builder: (context, constraints) { - return ResponsiveWidget( - responsiveUtils: _responsiveUtils, - mobile: Scaffold( - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - titleLanguageWidget, - const SizedBox(height: 8), - dropDownMenuSelectLanguage - ] - )), - desktop: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - titleLanguageWidget, - const SizedBox(height: 8), - SizedBox( - width: constraints.maxWidth / 2, - child: dropDownMenuSelectLanguage - ), - ])); - }); - } -} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/language_and_region/widgets/language_menu_overlay.dart b/lib/features/manage_account/presentation/language_and_region/widgets/language_menu_overlay.dart new file mode 100644 index 0000000000..ae01960779 --- /dev/null +++ b/lib/features/manage_account/presentation/language_and_region/widgets/language_menu_overlay.dart @@ -0,0 +1,66 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/widgets/lanuage_item_widget.dart'; + +class LanguageRegionOverlay extends StatelessWidget { + + final List listSupportedLanguages; + final Locale localeSelected; + final double? maxWidth; + final OnSelectLanguageAction onSelectLanguageAction; + + final _responsiveUtils = Get.find(); + + LanguageRegionOverlay({ + Key? key, + required this.listSupportedLanguages, + required this.localeSelected, + required this.onSelectLanguageAction, + this.maxWidth, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + constraints: BoxConstraints(maxHeight: _getHeightOverlay(context)), + width: maxWidth, + margin: const EdgeInsets.only(top: 4, bottom: 24), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: const [ + BoxShadow( + color: AppColor.colorShadowBgContentEmail, + blurRadius: 24, + offset: Offset(0, 8)), + BoxShadow( + color: AppColor.colorShadowBgContentEmail, + blurRadius: 2, + offset: Offset.zero), + ] + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: listSupportedLanguages.length, + itemBuilder: (context, index) => LanguageItemWidget( + localeSelected: localeSelected, + localeCurrent: listSupportedLanguages[index], + onSelectLanguageAction: onSelectLanguageAction + ) + ), + ); + } + + double _getHeightOverlay(BuildContext context) { + const double maxHeightTopBar = 80; + const double maxHeightTitleLanguage = 200; + const double paddingBottom = 16; + final currentHeight = _responsiveUtils.getSizeScreenHeight(context); + double maxHeightForm = currentHeight - maxHeightTopBar - maxHeightTitleLanguage - paddingBottom; + return maxHeightForm; + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/language_and_region/widgets/lanuage_item_widget.dart b/lib/features/manage_account/presentation/language_and_region/widgets/lanuage_item_widget.dart new file mode 100644 index 0000000000..50b17dfd8a --- /dev/null +++ b/lib/features/manage_account/presentation/language_and_region/widgets/lanuage_item_widget.dart @@ -0,0 +1,74 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/extensions/locale_extension.dart'; + +typedef OnSelectLanguageAction = Function(Locale? localeSelected); + +class LanguageItemWidget extends StatelessWidget { + + final Locale localeSelected; + final Locale localeCurrent; + final OnSelectLanguageAction onSelectLanguageAction; + + const LanguageItemWidget({ + super.key, + required this.localeCurrent, + required this.localeSelected, + required this.onSelectLanguageAction, + }); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onSelectLanguageAction.call(localeCurrent), + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: Row(children: [ + Expanded(child: Row( + children: [ + Text( + localeCurrent.getLanguageNameByCurrentLocale(context), + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.normal, + color: Colors.black + ), + maxLines: 1, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + ), + Text( + ' - ${localeCurrent.getSourceLanguageName()}', + style: const TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: Colors.black + ), + maxLines: 1, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + ) + ] + )), + if (localeCurrent == localeSelected) + SvgPicture.asset( + imagePaths.icChecked, + width: 20, + height: 20, + fit: BoxFit.fill + ) + ]), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/mailbox_visibility/bindings/mailbox_visibility_bindings.dart b/lib/features/manage_account/presentation/mailbox_visibility/bindings/mailbox_visibility_bindings.dart index f18e443a37..442ad85662 100644 --- a/lib/features/manage_account/presentation/mailbox_visibility/bindings/mailbox_visibility_bindings.dart +++ b/lib/features/manage_account/presentation/mailbox_visibility/bindings/mailbox_visibility_bindings.dart @@ -3,19 +3,23 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_i import 'package:tmail_ui_user/features/mailbox/domain/usecases/refresh_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/mailbox_visibility/bindings/mailbox_visibility_interactor_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart'; class MailboxVisibilityBindings extends Bindings { @override void dependencies() { + MailboxVisibilityInteractorBindings().dependencies(); + + _bindingsUtils(); + Get.lazyPut(() => MailboxVisibilityController( Get.find(), Get.find(), Get.find(), Get.find(), )); - _bindingsUtils(); } void _bindingsUtils() { diff --git a/lib/features/manage_account/presentation/mailbox_visibility/bindings/mailbox_visibility_interactor_bindings.dart b/lib/features/manage_account/presentation/mailbox_visibility/bindings/mailbox_visibility_interactor_bindings.dart index 31b12c149e..407d91591e 100644 --- a/lib/features/manage_account/presentation/mailbox_visibility/bindings/mailbox_visibility_interactor_bindings.dart +++ b/lib/features/manage_account/presentation/mailbox_visibility/bindings/mailbox_visibility_interactor_bindings.dart @@ -3,7 +3,7 @@ import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; -import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart'; diff --git a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart index 9a162035e2..172850a92f 100644 --- a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart +++ b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart @@ -1,7 +1,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; -import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; @@ -9,6 +9,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; @@ -25,7 +26,6 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/refresh_all_mailb import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories_expand_mode.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; @@ -40,16 +40,8 @@ class MailboxVisibilityController extends BaseMailboxController { final _accountDashBoardController = Get.find(); final _appToast = Get.find(); final _imagePaths = Get.find(); - final _responsiveUtils = Get.find(); - final mailboxCategoriesExpandMode = MailboxCategoriesExpandMode.initial().obs; final mailboxListScrollController = ScrollController(); - Map mapDefaultMailboxIdByRole = {}; - Map mapMailboxById = {}; - PresentationMailbox? outboxMailbox; - - jmap.State? _currentMailboxState; - MailboxVisibilityController( TreeBuilder treeBuilder, VerifyNameInteractor verifyNameInteractor, @@ -74,63 +66,63 @@ class MailboxVisibilityController extends BaseMailboxController { } @override - void onDone() { - viewState.value.fold((failure) {}, (success) { - if (success is GetAllMailboxSuccess) { - _currentMailboxState = success.currentMailboxState; - _buildMailboxTreeHasSubscribed(success.mailboxList); - } else if (success is RefreshChangesAllMailboxSuccess) { - _currentMailboxState = success.currentMailboxState; - _refreshMailboxTreeHasSubscribed(success.mailboxList); - } else if (success is SubscribeMailboxSuccess) { - _subscribeMailboxSuccess(success); - } else if (success is SubscribeMultipleMailboxAllSuccess) { - _handleUnsubscribeMultipleMailboxAllSuccess(success); - } else if (success is SubscribeMultipleMailboxHasSomeSuccess) { - _handleUnsubscribeMultipleMailboxHasSomeSuccess(success); + void handleSuccessViewState(Success success) async { + super.handleSuccessViewState(success); + if (success is GetAllMailboxSuccess) { + currentMailboxState = success.currentMailboxState; + _handleBuildTree(success.mailboxList); + } else if (success is RefreshChangesAllMailboxSuccess) { + currentMailboxState = success.currentMailboxState; + await refreshTree(success.mailboxList); + if (currentContext != null) { + await syncAllMailboxWithDisplayName(currentContext!); } - }); + } else if (success is SubscribeMailboxSuccess) { + _subscribeMailboxSuccess(success); + } else if (success is SubscribeMultipleMailboxAllSuccess) { + _handleUnsubscribeMultipleMailboxAllSuccess(success); + } else if (success is SubscribeMultipleMailboxHasSomeSuccess) { + _handleUnsubscribeMultipleMailboxHasSomeSuccess(success); + } } @override void onReady() { - final _session = _accountDashBoardController.sessionCurrent.value; - final _accountId = _accountDashBoardController.accountId.value; - if(_session != null && _accountId != null) { - getAllMailbox(_session, _accountId); + final session = _accountDashBoardController.sessionCurrent; + final accountId = _accountDashBoardController.accountId.value; + if(session != null && accountId != null) { + getAllMailbox(session, accountId); } super.onReady(); } - void _buildMailboxTreeHasSubscribed(List mailboxList) async { + void _handleBuildTree(List mailboxList) async { dispatchState(Right(LoadingBuildTreeMailboxVisibility())); - final _mailboxList = mailboxList; - await buildTree(_mailboxList); + await buildTree(mailboxList); dispatchState(Right(BuildTreeMailboxVisibilitySuccess())); - } - - void _refreshMailboxTreeHasSubscribed(List mailboxList) async { - final _mailboxList = mailboxList; - await refreshTree(_mailboxList); + if (currentContext != null) { + await syncAllMailboxWithDisplayName(currentContext!); + } } void subscribeMailbox(MailboxNode mailboxNode) { - final _mailboxSubscribeState = mailboxNode.item.isSubscribedMailbox + final mailboxSubscribeState = mailboxNode.item.isSubscribedMailbox ? MailboxSubscribeState.disabled : MailboxSubscribeState.enabled; - final _mailboxSubscribeStateAction = mailboxNode.item.isSubscribedMailbox + final mailboxSubscribeStateAction = mailboxNode.item.isSubscribedMailbox ? MailboxSubscribeAction.unSubscribe : MailboxSubscribeAction.subscribe; _subscribeMailboxAction( SubscribeMailboxRequest( mailboxNode.item.id, - _mailboxSubscribeState, - _mailboxSubscribeStateAction, + mailboxSubscribeState, + mailboxSubscribeStateAction, ) ); } void _subscribeMailboxAction(SubscribeMailboxRequest subscribeMailboxRequest) { - final _accountId = _accountDashBoardController.accountId.value; - if (_accountId != null) { + final accountId = _accountDashBoardController.accountId.value; + final session = _accountDashBoardController.sessionCurrent; + if (session != null && accountId != null) { final subscribeRequest = generateSubscribeRequest( subscribeMailboxRequest.mailboxId, subscribeMailboxRequest.subscribeState, @@ -138,9 +130,9 @@ class MailboxVisibilityController extends BaseMailboxController { ); if (subscribeRequest is SubscribeMultipleMailboxRequest) { - consumeState(_subscribeMultipleMailboxInteractor!.execute(_accountId, subscribeRequest)); + consumeState(_subscribeMultipleMailboxInteractor!.execute(session, accountId, subscribeRequest)); } else if (subscribeRequest is SubscribeMailboxRequest) { - consumeState(_subscribeMailboxInteractor!.execute(_accountId, subscribeRequest)); + consumeState(_subscribeMailboxInteractor!.execute(session, accountId, subscribeRequest)); } } } @@ -152,9 +144,9 @@ class MailboxVisibilityController extends BaseMailboxController { mailboxCategoriesExpandMode.value.defaultMailbox = newExpandMode; mailboxCategoriesExpandMode.refresh(); break; - case MailboxCategories.personalMailboxes: - final newExpandMode = mailboxCategoriesExpandMode.value.personalMailboxes == ExpandMode.EXPAND ? ExpandMode.COLLAPSE : ExpandMode.EXPAND; - mailboxCategoriesExpandMode.value.personalMailboxes = newExpandMode; + case MailboxCategories.personalFolders: + final newExpandMode = mailboxCategoriesExpandMode.value.personalFolders == ExpandMode.EXPAND ? ExpandMode.COLLAPSE : ExpandMode.EXPAND; + mailboxCategoriesExpandMode.value.personalFolders = newExpandMode; mailboxCategoriesExpandMode.refresh(); break; case MailboxCategories.teamMailboxes: @@ -174,7 +166,7 @@ class MailboxVisibilityController extends BaseMailboxController { _showToastSubscribeMailboxSuccess(subscribeMailboxSuccess.mailboxId); } - _refreshMailboxChanges(subscribeMailboxSuccess.currentEmailState); + _refreshMailboxChanges(subscribeMailboxSuccess.currentMailboxState); } void _handleUnsubscribeMultipleMailboxHasSomeSuccess(SubscribeMultipleMailboxHasSomeSuccess subscribeMailboxSuccess) { @@ -185,7 +177,7 @@ class MailboxVisibilityController extends BaseMailboxController { ); } - _refreshMailboxChanges(subscribeMailboxSuccess.currentEmailState); + _refreshMailboxChanges(subscribeMailboxSuccess.currentMailboxState); } void _handleUnsubscribeMultipleMailboxAllSuccess(SubscribeMultipleMailboxAllSuccess subscribeMailboxSuccess) { @@ -196,15 +188,15 @@ class MailboxVisibilityController extends BaseMailboxController { ); } - _refreshMailboxChanges(subscribeMailboxSuccess.currentEmailState); + _refreshMailboxChanges(subscribeMailboxSuccess.currentMailboxState); } - void _refreshMailboxChanges(jmap.State? currentEmailState) { - final _session = _accountDashBoardController.sessionCurrent.value; - final _accountId = _accountDashBoardController.accountId.value; - final currentMailboxState = currentEmailState ?? _currentMailboxState; - if (_session != null && _accountId != null && currentMailboxState != null) { - refreshMailboxChanges(_session, _accountId, currentMailboxState); + void _refreshMailboxChanges(jmap.State? newMailboxState) { + final session = _accountDashBoardController.sessionCurrent; + final accountId = _accountDashBoardController.accountId.value; + final mailboxState = newMailboxState ?? currentMailboxState; + if (session != null && accountId != null && mailboxState != null) { + refreshMailboxChanges(session, accountId, mailboxState); } } @@ -213,30 +205,29 @@ class MailboxVisibilityController extends BaseMailboxController { {List? listDescendantMailboxIds} ) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showBottomToast( - currentOverlayContext!, - AppLocalizations.of(currentContext!).toastMsgHideMailboxSuccess, - actionName: AppLocalizations.of(currentContext!).undo, - onActionClick: () => _subscribeMailboxAction( - SubscribeMailboxRequest( - mailboxIdSubscribed, - MailboxSubscribeState.enabled, - MailboxSubscribeAction.subscribe - ) - ), - leadingIcon: SvgPicture.asset( - _imagePaths.icFolderMailbox, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill - ), - backgroundColor: AppColor.toastSuccessBackgroundColor, - textColor: Colors.white, - textActionColor: Colors.white, - actionIcon: SvgPicture.asset(_imagePaths.icUndo), - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!) + _appToast.showToastMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).toastMsgHideMailboxSuccess, + actionName: AppLocalizations.of(currentContext!).undo, + onActionClick: () => _subscribeMailboxAction( + SubscribeMailboxRequest( + mailboxIdSubscribed, + MailboxSubscribeState.enabled, + MailboxSubscribeAction.subscribe + ) + ), + leadingSVGIconColor: Colors.white, + leadingSVGIcon: _imagePaths.icFolderMailbox, + backgroundColor: AppColor.toastSuccessBackgroundColor, + textColor: Colors.white, + actionIcon: SvgPicture.asset(_imagePaths.icUndo), ); } } + + @override + void onClose() { + mailboxListScrollController.dispose(); + super.onClose(); + } } diff --git a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart index 5088d2c1eb..b536af0eae 100644 --- a/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart +++ b/lib/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart @@ -1,69 +1,55 @@ -import 'package:core/core.dart'; import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/presentation/views/button/icon_button_web.dart'; import 'package:core/presentation/views/list/tree_view.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/mailbox/expand_mode.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/mixin/mailbox_widget_mixin.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_categories.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/base/setting_detail_view_builder.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/mailbox_visibility/state/mailbox_visibility_state.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/mailbox_visibility/utils/mailbox_visibility_utils.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/mailbox_visibility/widgets/mailbox_visibility_folder_tile_builder.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/mailbox_visibility/widgets/mailbox_visibility_header_widget.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -class MailboxVisibilityView extends GetWidget with AppLoaderMixin { +class MailboxVisibilityView extends GetWidget + with AppLoaderMixin, + MailboxWidgetMixin { + final _responsiveUtils = Get.find(); final _imagePaths = Get.find(); MailboxVisibilityView({Key? key}) : super(key: key); + @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: _responsiveUtils.isWebDesktop(context) - ? AppColor.colorBgDesktop - : Colors.white, - body: Container( - width: double.infinity, - margin: _responsiveUtils.isWebDesktop(context) - ? const EdgeInsets.all(24) - : EdgeInsets.symmetric(horizontal: SettingsUtils.getHorizontalPadding(context, _responsiveUtils)), - color: _responsiveUtils.isWebDesktop(context) ? null : Colors.white, - decoration: _responsiveUtils.isWebDesktop(context) - ? BoxDecoration( - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColor.colorBorderBodyThread, width: 1), - color: Colors.white) - : null, - child: ClipRRect( - borderRadius: BorderRadius.circular( - _responsiveUtils.isWebDesktop(context) ? 20 : 0), - child: Padding( - padding: const EdgeInsets.only(top: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (_responsiveUtils.isWebDesktop(context))...[ - const MailboxVisibilityHeaderWidget(), - const Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Divider(color: AppColor.colorDividerMailbox, height: 0.5, thickness: 0.2), - ), - ], - _buildLoadingView(), - Expanded(child: _buildListMailbox(context)), - ] - ), - ), - ), + return SettingDetailViewBuilder( + responsiveUtils: _responsiveUtils, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_responsiveUtils.isWebDesktop(context)) + ...[ + const SizedBox(height: 24), + const MailboxVisibilityHeaderWidget(), + const SizedBox(height: 16), + const Divider( + color: AppColor.colorDividerMailbox, + height: 0.5, + thickness: 0.2 + ) + ], + _buildLoadingView(), + Expanded(child: Padding( + padding: MailboxVisibilityUtils.getPaddingListView(context, _responsiveUtils), + child: _buildListMailbox(context) + )) + ] ), ); } @@ -71,102 +57,63 @@ class MailboxVisibilityView extends GetWidget with Widget _buildLoadingView() { return Obx(() => controller.viewState.value.fold( (failure) => const SizedBox.shrink(), - (success) { - if (success is LoadingState) { - return const Center( - child: Padding( - padding: EdgeInsets.only(top: 16), - child: SizedBox( - height: 24, - width: 24, - child: CupertinoActivityIndicator( - color: AppColor.colorTextButton, - ), - )), - ); - } else if (success is LoadingBuildTreeMailboxVisibility) { - return const Center( - child: Padding( - padding: EdgeInsets.only(top: 16), - child: SizedBox( - height: 24, - width: 24, - child: CupertinoActivityIndicator( - color: AppColor.colorTextButton, - ), - )), - ); - } - return const SizedBox.shrink(); - })); + (success) => success is LoadingState || success is LoadingBuildTreeMailboxVisibility + ? Padding(padding: const EdgeInsets.only(top: 16), child: loadingWidget) + : const SizedBox.shrink())); } Widget _buildListMailbox(BuildContext context) { return SingleChildScrollView( - controller: controller.mailboxListScrollController, - key: const PageStorageKey('mailbox_list'), - physics: const ClampingScrollPhysics(), - child: Column(children: [ - Obx(() => controller.defaultMailboxIsNotEmpty - ? _buildMailboxCategory(context, MailboxCategories.exchange, controller.defaultRootNode) - : const SizedBox.shrink()), - const SizedBox(height: 13), - Obx(() => controller.teamMailboxesIsNotEmpty - ? _buildMailboxCategory(context, MailboxCategories.teamMailboxes, controller.teamMailboxesRootNode) - : const SizedBox.shrink()), - const SizedBox(height: 8), - Obx(() => controller.personalMailboxIsNotEmpty - ? _buildMailboxCategory(context, MailboxCategories.personalMailboxes, controller.personalRootNode) - : const SizedBox.shrink()), - - ]) + controller: controller.mailboxListScrollController, + key: const PageStorageKey('mailbox_list'), + physics: const ClampingScrollPhysics(), + child: Column(children: [ + Obx(() => controller.defaultMailboxIsNotEmpty + ? _buildMailboxCategory( + context, + MailboxCategories.exchange, + controller.defaultRootNode) + : const SizedBox.shrink() + ), + Obx(() => controller.teamMailboxesIsNotEmpty + ? _buildMailboxCategory( + context, + MailboxCategories.teamMailboxes, + controller.teamMailboxesRootNode) + : const SizedBox.shrink() + ), + Obx(() => controller.personalMailboxIsNotEmpty + ? _buildMailboxCategory( + context, + MailboxCategories.personalFolders, + controller.personalRootNode) + : const SizedBox.shrink() + ) + ]) ); } - Widget _buildHeaderMailboxCategory(BuildContext context, MailboxCategories categories) { - return Padding( - padding: const EdgeInsets.only(top: 10), - child: Row(children: [ - buildIconWeb( - splashRadius: 5, - iconPadding: EdgeInsets.zero, - minSize: 12, - icon: SvgPicture.asset( - categories.getExpandMode(controller.mailboxCategoriesExpandMode.value) == ExpandMode.EXPAND - ? _imagePaths.icExpandFolder - : _imagePaths.icCollapseFolder, - color: AppColor.primaryColor, - fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).collapse, - onTap: () => controller.toggleMailboxCategories(categories)), - Expanded(child: Text(categories.getTitle(context), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 17, - color: Colors.black, - fontWeight: FontWeight.bold))), - ])); - } - Widget _buildMailboxCategory(BuildContext context, MailboxCategories categories, MailboxNode mailboxNode) { if (categories == MailboxCategories.exchange) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: _responsiveUtils.isDesktop(context) ? 24 : 0), - child: _buildBodyMailboxCategory(context, categories, mailboxNode), - ); + return _buildBodyMailboxCategory(context, categories, mailboxNode); } - return Padding( - padding: EdgeInsets.symmetric(horizontal: _responsiveUtils.isDesktop(context) ? 24 : 0), - child: Column(children: [ - _buildHeaderMailboxCategory(context, categories), - AnimatedContainer( - duration: const Duration(milliseconds: 400), - child: categories.getExpandMode(controller.mailboxCategoriesExpandMode.value) == ExpandMode.EXPAND - ? _buildBodyMailboxCategory(context, categories, mailboxNode) - : const Offstage()) - ]), - ); + return Column(children: [ + buildHeaderMailboxCategory( + context, + _responsiveUtils, + _imagePaths, + categories, + controller, + padding: const EdgeInsets.all(8), + toggleMailboxCategories: controller.toggleMailboxCategories + ), + AnimatedContainer( + duration: const Duration(milliseconds: 400), + child: categories.getExpandMode(controller.mailboxCategoriesExpandMode.value) == ExpandMode.EXPAND + ? _buildBodyMailboxCategory(context, categories, mailboxNode) + : const Offstage() + ) + ]); } Widget _buildBodyMailboxCategory( @@ -174,40 +121,40 @@ class MailboxVisibilityView extends GetWidget with MailboxCategories categories, MailboxNode mailboxNode, ) { - final lastNode = mailboxNode.childrenItems?.last; - - return Container( - padding: EdgeInsets.only( - right: _responsiveUtils.isDesktop(context) ? 0 : 12, - left: _responsiveUtils.isDesktop(context) ? 0 : 12), - child: TreeView( - key: Key('${categories.keyValue}_mailbox_list'), - children: _buildListChildTileWidget( - context, - mailboxNode, - lastNode: lastNode))); + return TreeView( + key: Key('${categories.keyValue}_mailbox_list'), + children: _buildListChildTileWidget(context, mailboxNode) + ); } List _buildListChildTileWidget( BuildContext context, MailboxNode parentNode, - {MailboxNode? lastNode} ) { return parentNode.childrenItems - ?.map((mailboxNode) => mailboxNode.hasChildren() - ? TreeViewChild( - context, - key: const Key('children_tree_mailbox_child'), - isExpanded: mailboxNode.expandMode == ExpandMode.EXPAND, - parent: (MailBoxVisibilityFolderTileBuilder(context, _imagePaths, mailboxNode, lastNode: lastNode) - ..addOnExpandFolderActionClick((mailboxNode) => controller.toggleMailboxFolder(mailboxNode)) - ..addOnSubscribeMailboxActionClick((mailboxNode) => controller.subscribeMailbox(mailboxNode))) - .build(context), - children: _buildListChildTileWidget(context, mailboxNode) - ).build() - : (MailBoxVisibilityFolderTileBuilder(context, _imagePaths, mailboxNode, lastNode: lastNode) - ..addOnExpandFolderActionClick((mailboxNode) => controller.toggleMailboxFolder(mailboxNode)) - ..addOnSubscribeMailboxActionClick((mailboxNode) => controller.subscribeMailbox(mailboxNode))) - .build(context)).toList() ?? []; + ?.map((mailboxNode) => mailboxNode.hasChildren() + ? TreeViewChild( + context, + key: const Key('children_tree_mailbox_child'), + isExpanded: mailboxNode.expandMode == ExpandMode.EXPAND, + paddingChild: const EdgeInsetsDirectional.only(start: 10), + parent: MailBoxVisibilityFolderTileBuilder( + _imagePaths, + mailboxNode, + onClickExpandMailboxNodeAction: (mailboxNode) { + controller.toggleMailboxFolder(mailboxNode, controller.mailboxListScrollController); + }, + onClickSubscribeMailboxAction: controller.subscribeMailbox + ), + children: _buildListChildTileWidget(context, mailboxNode)).build() + : MailBoxVisibilityFolderTileBuilder( + _imagePaths, + mailboxNode, + onClickExpandMailboxNodeAction: (mailboxNode) { + controller.toggleMailboxFolder(mailboxNode, controller.mailboxListScrollController); + }, + onClickSubscribeMailboxAction: controller.subscribeMailbox + )) + .toList() ?? []; } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/mailbox_visibility/state/mailbox_visibility_state.dart b/lib/features/manage_account/presentation/mailbox_visibility/state/mailbox_visibility_state.dart index 15b59175b4..7925bdfbc3 100644 --- a/lib/features/manage_account/presentation/mailbox_visibility/state/mailbox_visibility_state.dart +++ b/lib/features/manage_account/presentation/mailbox_visibility/state/mailbox_visibility_state.dart @@ -2,4 +2,4 @@ import 'package:core/presentation/state/success.dart'; class LoadingBuildTreeMailboxVisibility extends LoadingState {} -class BuildTreeMailboxVisibilitySuccess extends UIState {} \ No newline at end of file +class BuildTreeMailboxVisibilitySuccess extends UIState {} diff --git a/lib/features/manage_account/presentation/mailbox_visibility/utils/mailbox_visibility_utils.dart b/lib/features/manage_account/presentation/mailbox_visibility/utils/mailbox_visibility_utils.dart new file mode 100644 index 0000000000..4ccc6b5193 --- /dev/null +++ b/lib/features/manage_account/presentation/mailbox_visibility/utils/mailbox_visibility_utils.dart @@ -0,0 +1,14 @@ +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; + +class MailboxVisibilityUtils { + static EdgeInsets getPaddingListView(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isWebDesktop(context)) { + return const EdgeInsets.all(8); + } else if (responsiveUtils.isMobile(context) || responsiveUtils.isLandscapeMobile(context)) { + return const EdgeInsets.symmetric(horizontal: 16, vertical: 8); + } else { + return const EdgeInsets.symmetric(horizontal: 32, vertical: 8); + } + } +} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/mailbox_visibility/widgets/mailbox_visibility_folder_tile_builder.dart b/lib/features/manage_account/presentation/mailbox_visibility/widgets/mailbox_visibility_folder_tile_builder.dart index c0186e7b67..a86c725bfc 100644 --- a/lib/features/manage_account/presentation/mailbox_visibility/widgets/mailbox_visibility_folder_tile_builder.dart +++ b/lib/features/manage_account/presentation/mailbox_visibility/widgets/mailbox_visibility_folder_tile_builder.dart @@ -1,255 +1,161 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/expand_mode.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/extensions/presentation_mailbox_extension.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_node.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_method_action_define.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; + +class MailBoxVisibilityFolderTileBuilder extends StatelessWidget { -class MailBoxVisibilityFolderTileBuilder { final MailboxNode _mailboxNode; - final BuildContext _context; final ImagePaths _imagePaths; - final MailboxNode? lastNode; - final MailboxActions? mailboxActions; - - OnClickExpandMailboxNodeAction? _onClickExpandMailboxNodeAction; - OnClickSubscribeMailboxAction? _onClickSubscribeMailboxAction; - bool isHoverItem = false; + final OnClickExpandMailboxNodeAction? onClickExpandMailboxNodeAction; + final OnClickSubscribeMailboxAction? onClickSubscribeMailboxAction; - MailBoxVisibilityFolderTileBuilder( - this._context, + const MailBoxVisibilityFolderTileBuilder( this._imagePaths, this._mailboxNode, { - this.lastNode, - this.mailboxActions, - }); - - void addOnExpandFolderActionClick(OnClickExpandMailboxNodeAction onClickExpandMailboxNodeAction) { - _onClickExpandMailboxNodeAction = onClickExpandMailboxNodeAction; - } - - void addOnSubscribeMailboxActionClick(OnClickSubscribeMailboxAction onClickSubscribeMailboxAction) { - _onClickSubscribeMailboxAction = onClickSubscribeMailboxAction; - } - - Widget build(BuildContext context) => _buildMailboxItem(context); + Key? key, + this.onClickExpandMailboxNodeAction, + this.onClickSubscribeMailboxAction + }) : super(key: key); - Widget _buildMailboxItem(BuildContext context) { - if (BuildUtils.isWeb) { - return Theme( - data: ThemeData( - splashColor: Colors.transparent, - highlightColor: Colors.transparent), - child: StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - return InkWell( - onTap: () {}, - onHover: (value) => setState(() => isHoverItem = value), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: backgroundColorItem), - padding: EdgeInsets.only( - left: _mailboxNode.item.hasRole() ? 0 : 4, - right: 4, - top: 8, - bottom: 8), - margin: const EdgeInsets.only(bottom: 4), - child: Row( - children: [ - _buildLeadingMailboxItem(), - const SizedBox(width: 4), - Expanded(child: _buildTitleFolderItem()), - if (!_mailboxNode.item.hasRole() || !_mailboxNode.item.isSubscribedMailbox) - _buildSubscribeMailboxItem(context, _mailboxNode), - const SizedBox(width: 32), - ]), - ), - ); - }), - ); - } else { - return ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(14)), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: EdgeInsets.symmetric( - vertical: _mailboxNode.hasChildren() ? 8 : 15), - child: Row( - crossAxisAlignment: - _mailboxNode.item.isTeamMailboxes - ? CrossAxisAlignment.start - : CrossAxisAlignment.center, - children: [ - if (_mailboxNode.item.isTeamMailboxes) - const SizedBox(width: 16), - _buildLeadingMailboxItem(), - const SizedBox(width: 8), - Expanded(child: _buildTitleFolderItem()), - if (!_mailboxNode.item.hasRole() || !_mailboxNode.item.isSubscribedMailbox) - _buildSubscribeMailboxItem(context, _mailboxNode), - ]), - ), - ])); - } + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () {}, + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Padding( + padding: const EdgeInsets.all(8), + child: Row( + children: [ + const SizedBox(width: 8), + _buildLeadingMailboxItem(context), + Expanded(child: _buildTitleFolderItem(context)), + if (!_mailboxNode.item.isDefault) + _buildSubscribeButton(context), + const SizedBox(width: 32), + ] + ), + ), + ), + ); } - Widget _buildLeadingMailboxItem() { - if (BuildUtils.isWeb) { - return Row(mainAxisSize: MainAxisSize.min, children: [ + Widget _buildLeadingMailboxItem(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ if (_mailboxNode.hasChildren() && _mailboxNode.item.isPersonal) - Row( - children: [ - const SizedBox(width: 8), - buildIconWeb( - icon: SvgPicture.asset( - _mailboxNode.expandMode == ExpandMode.EXPAND - ? _imagePaths.icExpandFolder - : _imagePaths.icCollapseFolder, - color: _mailboxNode.expandMode == ExpandMode.EXPAND - ? AppColor.colorExpandMailbox - : AppColor.colorCollapseMailbox, - fit: BoxFit.fill), - minSize: 12, - splashRadius: 10, - iconPadding: EdgeInsets.zero, - tooltip: _mailboxNode.expandMode == ExpandMode.EXPAND - ? AppLocalizations.of(_context).collapse - : AppLocalizations.of(_context).expand, - onTap: () => _onClickExpandMailboxNodeAction?.call(_mailboxNode)), - ], - ) - else - SizedBox(width: _mailboxNode.item.isPersonal ? 32 : 24), - Transform( - transform: Matrix4.translationValues(-4.0, 0.0, 0.0), - child: _buildLeadingIconTeamMailboxes()), - ]); - } else { - return Row(mainAxisSize: MainAxisSize.min, children: [ - if (_mailboxNode.hasChildren()) - Row( - children: [ - SizedBox(width: _mailboxNode.item.hasRole() ? 0 : 0), - if (!_mailboxNode.item.isTeamMailboxes) - buildIconWeb( - icon: SvgPicture.asset( - _mailboxNode.expandMode == ExpandMode.EXPAND - ? _imagePaths.icExpandFolder - : _imagePaths.icCollapseFolder, - color: _mailboxNode.expandMode == ExpandMode.EXPAND - ? AppColor.colorExpandMailbox - : AppColor.colorCollapseMailbox, - fit: BoxFit.fill), - minSize: 12, - splashRadius: 10, - iconPadding: EdgeInsets.zero, - tooltip: _mailboxNode.expandMode == ExpandMode.EXPAND - ? AppLocalizations.of(_context).collapse - : AppLocalizations.of(_context).expand, - onTap: () => _onClickExpandMailboxNodeAction?.call(_mailboxNode)), - ], + buildIconWeb( + splashRadius: 12, + iconPadding: EdgeInsets.zero, + minSize: 12, + icon: SvgPicture.asset( + _mailboxNode.expandMode == ExpandMode.EXPAND + ? _imagePaths.icExpandFolder + : DirectionUtils.isDirectionRTLByLanguage(context) ? _imagePaths.icBack : _imagePaths.icCollapseFolder, + colorFilter: _mailboxNode.item.allowedToDisplay + ? AppColor.primaryColor.asFilter() + : AppColor.colorIconUnSubscribedMailbox.asFilter(), + fit: BoxFit.fill + ), + tooltip: _mailboxNode.expandMode == ExpandMode.EXPAND + ? AppLocalizations.of(context).collapse + : AppLocalizations.of(context).expand, + onTap: () => onClickExpandMailboxNodeAction?.call(_mailboxNode) ) else const SizedBox(width: 24), - _buildLeadingIconTeamMailboxes(), - ]); - } - } - - Widget _buildLeadingIconTeamMailboxes() { - if (!_mailboxNode.item.isPersonal) { - return _buildLeadingIconForChildOfTeamMailboxes(); - } else { - return _buildMailboxIcon(); - } - } - - Widget _buildLeadingIconForChildOfTeamMailboxes() { - if (_mailboxNode.item.hasParentId()) { - return _buildMailboxIcon(); - } else { - return const SizedBox(); - } - } - - Color get backgroundColorItem { - if (BuildUtils.isWeb && isHoverItem) { - return AppColor.colorBgMailboxSelected; - } else { - return Colors.white; - } + if (!_mailboxNode.item.isTeamMailboxes) + _buildMailboxIcon(context), + ] + ); } - Widget _buildTitleFolderItem() { + Widget _buildTitleFolderItem(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - _mailboxNode.item.name?.name ?? '', + _mailboxNode.item.getDisplayName(context), maxLines: 1, softWrap: CommonTextStyle.defaultSoftWrap, overflow: CommonTextStyle.defaultTextOverFlow, style: TextStyle( fontSize: _mailboxNode.item.isTeamMailboxes ? 16 : 15, - color: _mailboxNode.item.isSubscribedMailbox - ? _getColorTextTitleFolderItem() - : AppColor.colorItemAlreadySelected, + color: _mailboxNode.item.allowedToDisplay + ? Colors.black + : AppColor.colorTitleAUnSubscribedMailbox, fontWeight: _mailboxNode.item.isTeamMailboxes ? FontWeight.w500 : FontWeight.normal), ), if (_mailboxNode.item.isTeamMailboxes) Text( - _mailboxNode.item.emailTeamMailBoxes ?? '', + _mailboxNode.item.emailTeamMailBoxes, maxLines: 1, softWrap: CommonTextStyle.defaultSoftWrap, overflow: CommonTextStyle.defaultTextOverFlow, style: const TextStyle( fontSize: 13, color: AppColor.colorEmailAddressFull, - fontWeight: FontWeight.w400), + fontWeight: FontWeight.normal), ), ], ); } - Color _getColorTextTitleFolderItem() { - if (_mailboxNode.item.isTeamMailboxes) { - return Colors.black; - } else { - return AppColor.colorNameEmail; - } - } - - Widget _buildMailboxIcon() { - return SvgPicture.asset(_mailboxNode.item.getMailboxIcon(_imagePaths), - width: BuildUtils.isWeb ? 20 : 24, - height: BuildUtils.isWeb ? 20 : 24, - color: (!_mailboxNode.item.isSubscribedMailbox) - ? AppColor.colorDeleteContactIcon - : null, - fit: BoxFit.fill); + Widget _buildMailboxIcon(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) ? 0 : 8, + left: AppUtils.isDirectionRTL(context) ? 8 : 0, + ), + child: SvgPicture.asset( + _mailboxNode.item.getMailboxIcon(_imagePaths), + width: 20, + height: 20, + colorFilter: _mailboxNode.item.allowedToDisplay + ? AppColor.primaryColor.asFilter() + : AppColor.colorIconUnSubscribedMailbox.asFilter(), + fit: BoxFit.fill + ), + ); } - Widget _buildSubscribeMailboxItem(BuildContext context, MailboxNode _mailboxNode) { - return InkWell( - onTap: () => _onClickSubscribeMailboxAction?.call(_mailboxNode), - child: Text( - _mailboxNode.item.isSubscribedMailbox - ? AppLocalizations.of(context).hide - : AppLocalizations.of(context).show, - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: const TextStyle( - fontSize: 15, color: Colors.blue, fontWeight: FontWeight.normal), + Widget _buildSubscribeButton(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onClickSubscribeMailboxAction?.call(_mailboxNode), + borderRadius: const BorderRadius.all(Radius.circular(5)), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + child: Text( + _mailboxNode.item.isSubscribedMailbox + ? AppLocalizations.of(context).hide + : AppLocalizations.of(context).show, + maxLines: 1, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + style: const TextStyle( + fontSize: 15, + color: AppColor.primaryColor, + fontWeight: FontWeight.normal + ) + ), + ), ), ); } diff --git a/lib/features/manage_account/presentation/mailbox_visibility/widgets/mailbox_visibility_header_widget.dart b/lib/features/manage_account/presentation/mailbox_visibility/widgets/mailbox_visibility_header_widget.dart index db732aeb46..6f5f69a0eb 100644 --- a/lib/features/manage_account/presentation/mailbox_visibility/widgets/mailbox_visibility_header_widget.dart +++ b/lib/features/manage_account/presentation/mailbox_visibility/widgets/mailbox_visibility_header_widget.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:flutter/material.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; class MailboxVisibilityHeaderWidget extends StatelessWidget { @@ -9,7 +10,10 @@ class MailboxVisibilityHeaderWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only(left: 24), + padding: EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 0 : 24, + right: AppUtils.isDirectionRTL(context) ? 24 : 0 + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_bindings.dart b/lib/features/manage_account/presentation/manage_account_dashboard_bindings.dart index fb55c1fbdf..4824aeab0a 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_bindings.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_bindings.dart @@ -1,36 +1,17 @@ import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/account_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/hive_account_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; -import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; -import 'package:tmail_ui_user/features/login/data/repository/account_repository_impl.dart'; -import 'package:tmail_ui_user/features/login/data/repository/authentication_oidc_repository_impl.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/get_credential_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_token_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/data/datasource/manage_account_datasource.dart'; import 'package:tmail_ui_user/features/manage_account/data/datasource_impl/manage_account_datasource_impl.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; import 'package:tmail_ui_user/features/manage_account/data/repository/manage_account_repository_impl.dart'; import 'package:tmail_ui_user/features/manage_account/domain/repository/manage_account_repository.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/language_and_region_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/manage_account_dashboard_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/manage_account_menu_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings/settings_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/profiles_bindings.dart'; -import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; class ManageAccountDashBoardBindings extends BaseBindings { @@ -41,38 +22,23 @@ class ManageAccountDashBoardBindings extends BaseBindings { SettingsBindings().dependencies(); ManageAccountMenuBindings().dependencies(); ProfileBindings().dependencies(); - LanguageAndRegionBindings().dependencies(); } @override void bindingsController() { - Get.lazyPut(() => ManageAccountDashBoardController( - Get.find(), - Get.find(), - Get.find(), - Get.find() + Get.put(ManageAccountDashBoardController( + Get.find(), + Get.find() )); } @override void bindingsDataSource() { - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); } @override void bindingsDataSourceImpl() { - Get.lazyPut(() => HiveAccountDatasourceImpl( - Get.find(), - Get.find())); - Get.lazyPut(() => AuthenticationOIDCDataSourceImpl( - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find() - )); Get.lazyPut(() => ManageAccountDataSourceImpl( Get.find(), Get.find())); @@ -80,36 +46,16 @@ class ManageAccountDashBoardBindings extends BaseBindings { @override void bindingsInteractor() { - Get.lazyPut(() => LogoutOidcInteractor( - Get.find(), - Get.find(), - )); - Get.lazyPut(() => DeleteAuthorityOidcInteractor( - Get.find(), - Get.find())); - Get.lazyPut(() => GetStoredTokenOidcInteractor( - Get.find(), - Get.find(), - )); - Get.lazyPut(() => GetAuthenticatedAccountInteractor( - Get.find(), - Get.find(), - Get.find(), - )); Get.lazyPut(() => UpdateAuthenticationAccountInteractor(Get.find())); } @override void bindingsRepository() { - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); } @override void bindingsRepositoryImpl() { - Get.lazyPut(() => AccountRepositoryImpl(Get.find())); - Get.lazyPut(() => AuthenticationOIDCRepositoryImpl(Get.find())); Get.lazyPut(() => ManageAccountRepositoryImpl(Get.find())); } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart index d5dfc4c548..d2fc469801 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_controller.dart @@ -13,23 +13,23 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:rule_filter/rule_filter/capability_rule_filter.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/get_user_profile_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/update_vacation_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_vacation_interactor.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/update_vacation_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/action/dashboard_setting_action.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/email_rules/bindings/email_rules_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/forward/bindings/forward_bindings.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/language_and_region/language_and_region_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/mailbox_visibility/bindings/mailbox_visibility_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/account_menu_item.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/manage_account_arguments.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/settings_page_level.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/profiles/profiles_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_controller_bindings.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -41,86 +41,68 @@ import 'package:tmail_ui_user/main/routes/route_utils.dart'; class ManageAccountDashBoardController extends ReloadableController { final _appToast = Get.find(); - final _imagePaths = Get.find(); final _responsiveUtils = Get.find(); GetAllVacationInteractor? _getAllVacationInteractor; UpdateVacationInteractor? _updateVacationInteractor; - final menuDrawerKey = GlobalKey(debugLabel: 'manage_account'); - final appInformation = Rxn(); final userProfile = Rxn(); final accountId = Rxn(); final accountMenuItemSelected = AccountMenuItem.profiles.obs; final settingsPageLevel = SettingsPageLevel.universal.obs; - final sessionCurrent = Rxn(); final vacationResponse = Rxn(); final dashboardSettingAction = Rxn(); + Session? sessionCurrent; + ManageAccountDashBoardController( - LogoutOidcInteractor logoutOidcInteractor, - DeleteAuthorityOidcInteractor deleteAuthorityOidcInteractor, GetAuthenticatedAccountInteractor getAuthenticatedAccountInteractor, UpdateAuthenticationAccountInteractor updateAuthenticationAccountInteractor ) : super( - logoutOidcInteractor, - deleteAuthorityOidcInteractor, getAuthenticatedAccountInteractor, updateAuthenticationAccountInteractor ); @override void onReady() { + _initialPageLevel(); _getArguments(); _getAppVersion(); - _initialPageLevel(); super.onReady(); } @override - void onDone() { - viewState.value.fold( - (failure) {}, - (success) { - if (success is GetUserProfileSuccess) { - userProfile.value = success.userProfile; - } else if (success is GetAllVacationSuccess) { - if (success.listVacationResponse.isNotEmpty) { - vacationResponse.value = success.listVacationResponse.first; - } - } else if (success is UpdateVacationSuccess) { - _handleUpdateVacationSuccess(success); - } + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetUserProfileSuccess) { + userProfile.value = success.userProfile; + } else if (success is GetAllVacationSuccess) { + if (success.listVacationResponse.isNotEmpty) { + vacationResponse.value = success.listVacationResponse.first; } - ); + } else if (success is UpdateVacationSuccess) { + _handleUpdateVacationSuccess(success); + } } @override void handleReloaded(Session session) { + sessionCurrent = session; accountId.value = session.accounts.keys.first; - sessionCurrent.value = session; _getUserProfile(); - injectAutoCompleteBindings(sessionCurrent.value, accountId.value); - injectForwardBindings(sessionCurrent.value, accountId.value); - injectRuleFilterBindings(sessionCurrent.value, accountId.value); - injectMailboxVisibilityBindings(); - injectVacationBindings(sessionCurrent.value, accountId.value); + _bindingInteractorForMenuItemView(sessionCurrent, accountId.value); _getVacationResponse(); } void _getArguments() { final arguments = Get.arguments; - log('ManageAccountDashBoardController::_getAccountIdAndUserProfile(): $arguments'); + log('ManageAccountDashBoardController::_getArguments(): $arguments'); if (arguments is ManageAccountArguments) { + sessionCurrent = arguments.session; accountId.value = arguments.session?.accounts.keys.first; - sessionCurrent.value = arguments.session; _getUserProfile(); - injectAutoCompleteBindings(sessionCurrent.value, accountId.value); - injectForwardBindings(sessionCurrent.value, accountId.value); - injectRuleFilterBindings(sessionCurrent.value, accountId.value); - injectMailboxVisibilityBindings(); - injectVacationBindings(sessionCurrent.value, accountId.value); + _bindingInteractorForMenuItemView(sessionCurrent, accountId.value); _getVacationResponse(); if (arguments.menuSettingCurrent != null) { _goToSettingMenuCurrent(arguments.menuSettingCurrent!); @@ -140,6 +122,13 @@ class ManageAccountDashBoardController extends ReloadableController { } } + void _bindingInteractorForMenuItemView(Session? session, AccountId? accountId) { + injectAutoCompleteBindings(session, accountId); + injectVacationBindings(session, accountId); + injectForwardBindings(session, accountId); + injectRuleFilterBindings(session, accountId); + } + @override void injectVacationBindings(Session? session, AccountId? accountId) { try { @@ -159,8 +148,8 @@ class ManageAccountDashBoardController extends ReloadableController { } void _getUserProfile() async { - log('ManageAccountDashBoardController::_getUserProfile(): ${sessionCurrent.value}'); - userProfile.value = sessionCurrent.value != null ? UserProfile(sessionCurrent.value!.username.value) : null; + log('ManageAccountDashBoardController::_getUserProfile(): $sessionCurrent'); + userProfile.value = sessionCurrent != null ? UserProfile(sessionCurrent!.username.value) : null; } void _getVacationResponse() { @@ -173,30 +162,36 @@ class ManageAccountDashBoardController extends ReloadableController { vacationResponse.value = newVacation; } - void openMenuDrawer() { - menuDrawerKey.currentState?.openDrawer(); - } - - void closeMenuDrawer() { - menuDrawerKey.currentState?.openEndDrawer(); - } - - bool get isMenuDrawerOpen => menuDrawerKey.currentState?.isDrawerOpen == true; - void selectAccountMenuItem(AccountMenuItem newAccountMenuItem) { + settingsPageLevel.value = newAccountMenuItem == AccountMenuItem.none + ? SettingsPageLevel.universal + : SettingsPageLevel.level1; + clearInputFormView(); - if (newAccountMenuItem == AccountMenuItem.emailRules) { - EmailRulesBindings().dependencies(); - } - if (newAccountMenuItem == AccountMenuItem.forward) { - ForwardBindings().dependencies(); - } - if (newAccountMenuItem == AccountMenuItem.mailboxVisibility) { - MailboxVisibilityBindings().dependencies(); - } + _bindingControllerMenuItemView(newAccountMenuItem); accountMenuItemSelected.value = newAccountMenuItem; - if (isMenuDrawerOpen) { - closeMenuDrawer(); + } + + void _bindingControllerMenuItemView(AccountMenuItem item) { + switch (item) { + case AccountMenuItem.profiles: + ProfileBindings().dependencies(); + break; + case AccountMenuItem.languageAndRegion: + LanguageAndRegionBindings().dependencies(); + break; + case AccountMenuItem.emailRules: + EmailRulesBindings().dependencies(); + break; + case AccountMenuItem.forward: + ForwardBindings().dependencies(); + break; + case AccountMenuItem.mailboxVisibility: + MailboxVisibilityBindings().dependencies(); + break; + case AccountMenuItem.vacation: + case AccountMenuItem.none: + break; } } @@ -211,31 +206,15 @@ class ManageAccountDashBoardController extends ReloadableController { } void _goToSettingMenuCurrent(AccountMenuItem accountMenuItem) { - if (accountMenuItem == AccountMenuItem.emailRules) { - EmailRulesBindings().dependencies(); - } - if (accountMenuItem == AccountMenuItem.forward) { - ForwardBindings().dependencies(); - } - if (accountMenuItem == AccountMenuItem.mailboxVisibility) { - MailboxVisibilityBindings().dependencies(); - } - accountMenuItemSelected.value = accountMenuItem; - if (currentContext != null && - !_responsiveUtils.isDesktop(currentContext!)) { - settingsPageLevel.value = SettingsPageLevel.level1; - } + selectAccountMenuItem(accountMenuItem); } void goToSettings() { - pushAndPop(AppRoutes.settings, - arguments: ManageAccountArguments(sessionCurrent.value)); + popAndPush(AppRoutes.settings, + arguments: ManageAccountArguments(sessionCurrent)); } void backToMailboxDashBoard(BuildContext context) { - if (isMenuDrawerOpen) { - closeMenuDrawer(); - } if (canBack(context)) { popBack(result: vacationResponse.value); } else { @@ -246,32 +225,26 @@ class ManageAccountDashBoardController extends ReloadableController { } } - bool checkAvailableVacationInSession() { - try { - requireCapability(sessionCurrent.value!, accountId.value!, [CapabilityIdentifier.jmapVacationResponse]); - return true; - } catch(e) { - logError('ManageAccountDashBoardController::checkAvailableVacationInSession(): exception = $e'); + bool get isVacationCapabilitySupported { + if (accountId.value != null && sessionCurrent != null) { + return [CapabilityIdentifier.jmapVacationResponse].isSupported(sessionCurrent!, accountId.value!); + } else { return false; } } - bool checkAvailableRuleFilterInSession() { - try { - requireCapability(sessionCurrent.value!, accountId.value!, [capabilityRuleFilter]); - return true; - } catch(e) { - logError('ManageAccountDashBoardController::checkAvailableRuleFilterInSession(): exception = $e'); + bool get isRuleFilterCapabilitySupported { + if (accountId.value != null && sessionCurrent != null) { + return [capabilityRuleFilter].isSupported(sessionCurrent!, accountId.value!); + } else { return false; } } - bool checkAvailableForwardInSession() { - try { - requireCapability(sessionCurrent.value!, accountId.value!, [capabilityForward]); - return true; - } catch(e) { - logError('ManageAccountDashBoardController::checkAvailableRuleFilterInSession(): exception = $e'); + bool get isForwardCapabilitySupported { + if (accountId.value != null && sessionCurrent != null) { + return [capabilityForward].isSupported(sessionCurrent!, accountId.value!); + } else { return false; } } @@ -288,10 +261,9 @@ class ManageAccountDashBoardController extends ReloadableController { void _handleUpdateVacationSuccess(UpdateVacationSuccess success) { if (success.listVacationResponse.isNotEmpty) { if (currentContext != null && currentOverlayContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - message: AppLocalizations.of(currentContext!).yourVacationResponderIsDisabledSuccessfully, - icon: _imagePaths.icChecked); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).yourVacationResponderIsDisabledSuccessfully); } vacationResponse.value = success.listVacationResponse.first; log('ManageAccountDashBoardController::_handleUpdateVacationSuccess(): $vacationResponse'); @@ -312,4 +284,11 @@ class ManageAccountDashBoardController extends ReloadableController { dashboardSettingAction.value = newAction; } } + + Future backButtonPressedCallbackAction(BuildContext context) async { + if (PlatformInfo.isMobile) { + backToMailboxDashBoard(context); + } + return false; + } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart index cf2460d97c..6b552547c1 100644 --- a/lib/features/manage_account/presentation/manage_account_dashboard_view.dart +++ b/lib/features/manage_account/presentation/manage_account_dashboard_view.dart @@ -5,12 +5,13 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/image/avatar_builder.dart'; import 'package:core/presentation/views/responsive/responsive_widget.dart'; import 'package:core/presentation/views/text/slogan_builder.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/user_setting_popup_menu_mixin.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/mailbox_visibility/mailbox_visibility_view.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/email_rules/email_rules_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/forward/forward_view.dart'; @@ -34,97 +35,93 @@ class ManageAccountDashBoardView extends GetWidget FocusScope.of(context).unfocus(), - child: ResponsiveWidget( - responsiveUtils: _responsiveUtils, - desktop: Column(children: [ - Row(children: [ - Container(width: 256, color: Colors.white, - padding: const EdgeInsets.only(top: 25, bottom: 25, left: 32), - child: Row(children: [ - (SloganBuilder(arrangedByHorizontal: true) - ..setSloganText(AppLocalizations.of(context).app_name) - ..setSloganTextAlign(TextAlign.center) - ..setSloganTextStyle(const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold)) - ..setSizeLogo(24) - ..addOnTapCallback(() => controller.backToMailboxDashBoard(context)) - ..setLogo(_imagePaths.icLogoTMail)) - .build(), - Obx(() { - if (controller.appInformation.value != null) { - return Padding(padding: const EdgeInsets.only(top: 6), - child: Text( - 'v.${controller.appInformation.value!.version}', - textAlign: TextAlign.center, - style: const TextStyle(fontSize: 13, color: AppColor.colorContentEmail, fontWeight: FontWeight.w500), - )); - } else { - return const SizedBox.shrink(); - } - }), - ]) - ), - Expanded(child: Padding( - padding: const EdgeInsets.only(right: 10, top: 16, bottom: 10, left: 48), - child: _buildRightHeader(context))) + return WillPopScope( + onWillPop: () => controller.backButtonPressedCallbackAction.call(context), + child: Scaffold( + backgroundColor: Colors.white, + drawerEnableOpenDragGesture: false, + body: GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: ResponsiveWidget( + responsiveUtils: _responsiveUtils, + desktop: Column(children: [ + Row(children: [ + Container(width: 256, color: Colors.white, + padding: SettingsUtils.getPaddingHeaderSetting(context), + child: Row(children: [ + SloganBuilder( + sizeLogo: 24, + text: AppLocalizations.of(context).app_name, + textAlign: TextAlign.center, + textStyle: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold), + logoSVG: _imagePaths.icTMailLogo, + onTapCallback: () => controller.backToMailboxDashBoard(context), + ), + Obx(() { + if (controller.appInformation.value != null) { + return Padding(padding: const EdgeInsets.only(top: 6), + child: Text( + 'v.${controller.appInformation.value!.version}', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 13, color: AppColor.colorContentEmail, fontWeight: FontWeight.w500), + )); + } else { + return const SizedBox.shrink(); + } + }), + ]) + ), + Expanded(child: Padding( + padding: SettingsUtils.getPaddingRightHeaderSetting(context), + child: _buildRightHeader(context))) + ]), + Expanded(child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(width: ResponsiveUtils.defaultSizeMenu, child: ManageAccountMenuView()), + Expanded(child: Container( + color: AppColor.colorBgDesktop, + child: Column(children: [ + Obx(() { + if (controller.vacationResponse.value?.vacationResponderIsValid == true) { + return VacationNotificationMessageWidget( + margin: const EdgeInsets.only( + top: 16, + left: PlatformInfo.isWeb ? 24 : 16, + right: PlatformInfo.isWeb ? 24 : 16), + fromAccountDashBoard: true, + vacationResponse: controller.vacationResponse.value!, + actionGotoVacationSetting: !controller.inVacationSettings() + ? () => controller.selectAccountMenuItem(AccountMenuItem.vacation) + : null, + actionEndNow: () => controller.disableVacationResponder()); + } else if ((controller.vacationResponse.value?.vacationResponderIsWaiting == true + || controller.vacationResponse.value?.vacationResponderIsStopped == true) + && controller.accountMenuItemSelected.value == AccountMenuItem.vacation) { + return VacationNotificationMessageWidget( + margin: const EdgeInsets.only( + top: 16, + left: PlatformInfo.isWeb ? 24 : 16, + right: PlatformInfo.isWeb ? 24 : 16), + fromAccountDashBoard: true, + vacationResponse: controller.vacationResponse.value!, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + leadingIcon: const Padding( + padding: EdgeInsets.only(right: 16), + child: Icon(Icons.timer, size: 20), + )); + } else { + return const SizedBox.shrink(); + } + }), + Expanded(child: _viewDisplayedOfAccountMenuItem()) + ]), + )) + ], + )) ]), - Expanded(child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(child: ManageAccountMenuView(), width: ResponsiveUtils.defaultSizeMenu), - Expanded(child: Container( - color: AppColor.colorBgDesktop, - child: Column(children: [ - Obx(() { - if (controller.vacationResponse.value?.vacationResponderIsValid == true) { - return VacationNotificationMessageWidget( - margin: const EdgeInsets.only( - top: 16, - left: BuildUtils.isWeb ? 24 : 16, - right: BuildUtils.isWeb ? 24 : 16), - fromAccountDashBoard: true, - vacationResponse: controller.vacationResponse.value!, - actionGotoVacationSetting: !controller.inVacationSettings() - ? () => controller.selectAccountMenuItem(AccountMenuItem.vacation) - : null, - actionEndNow: () => controller.disableVacationResponder()); - } else if ((controller.vacationResponse.value?.vacationResponderIsWaiting == true - || controller.vacationResponse.value?.vacationResponderIsStopped == true) - && controller.accountMenuItemSelected.value == AccountMenuItem.vacation) { - return VacationNotificationMessageWidget( - margin: const EdgeInsets.only( - top: 16, - left: BuildUtils.isWeb ? 24 : 16, - right: BuildUtils.isWeb ? 24 : 16), - fromAccountDashBoard: true, - vacationResponse: controller.vacationResponse.value!, - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), - leadingIcon: const Padding( - padding: EdgeInsets.only(right: 16), - child: Icon(Icons.timer, size: 20), - )); - } else { - return const SizedBox.shrink(); - } - }), - Expanded(child: _viewDisplayedOfAccountMenuItem()) - ]), - )) - ], - )) - ]), - mobile: SettingsView(closeAction: () => controller.backToMailboxDashBoard(context)) + mobile: SettingsView(closeAction: () => controller.backToMailboxDashBoard(context)) + ), ), ), ); @@ -144,7 +141,7 @@ class ManageAccountDashBoardView extends GetWidget ManageAccountMenuController()); - } - - @override - void bindingsDataSource() { - } - - @override - void bindingsDataSourceImpl() { - } - - @override - void bindingsInteractor() { - } - - @override - void bindingsRepository() { - } - - @override - void bindingsRepositoryImpl() { + void dependencies() { + Get.put(ManageAccountMenuController()); } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/menu/manage_account_menu_controller.dart b/lib/features/manage_account/presentation/menu/manage_account_menu_controller.dart index adb797201e..b31c483b89 100644 --- a/lib/features/manage_account/presentation/menu/manage_account_menu_controller.dart +++ b/lib/features/manage_account/presentation/menu/manage_account_menu_controller.dart @@ -1,68 +1,59 @@ +import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/manage_account_dashboard_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/account_menu_item.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -class ManageAccountMenuController extends BaseController { +class ManageAccountMenuController extends GetxController { final dashBoardController = Get.find(); - - late Worker sessionWorker; + final _responsiveUtils = Get.find(); final listAccountMenuItem = RxList([ AccountMenuItem.profiles, - AccountMenuItem.vacation, + AccountMenuItem.mailboxVisibility, AccountMenuItem.languageAndRegion, ]); - void _initWorker() { - sessionWorker = ever(dashBoardController.sessionCurrent, (_) { - _createListAccountMenu(); + void _registerObxStreamListener() { + ever(dashBoardController.accountId, (accountId) { + if (accountId != null) { + _createListAccountMenu(); + } }); } - void _clearWorker() { - sessionWorker.call(); - } - @override void onInit() { - _initWorker(); - _createListAccountMenu(); + _registerObxStreamListener(); super.onInit(); } - void _createListAccountMenu(){ - listAccountMenuItem.clear(); - listAccountMenuItem.add(AccountMenuItem.profiles); - if (dashBoardController.checkAvailableRuleFilterInSession()) { - listAccountMenuItem.add(AccountMenuItem.emailRules); - } - if (dashBoardController.checkAvailableForwardInSession()) { - listAccountMenuItem.add(AccountMenuItem.forward); - } - if (dashBoardController.checkAvailableVacationInSession()) { - listAccountMenuItem.add(AccountMenuItem.vacation); - } - listAccountMenuItem.addAll( - [ - AccountMenuItem.mailboxVisibility, - AccountMenuItem.languageAndRegion - ] - ); - } + void _createListAccountMenu() { + final newListMenuSetting = [ + AccountMenuItem.profiles, + if (dashBoardController.isRuleFilterCapabilitySupported) + AccountMenuItem.emailRules, + if (dashBoardController.isForwardCapabilitySupported) + AccountMenuItem.forward, + if (dashBoardController.isVacationCapabilitySupported) + AccountMenuItem.vacation, + AccountMenuItem.mailboxVisibility, + AccountMenuItem.languageAndRegion + ]; + listAccountMenuItem.value = newListMenuSetting; - @override - void onClose() { - _clearWorker(); - super.onClose(); + if (listAccountMenuItem.isNotEmpty) { + if (currentContext != null && _responsiveUtils.isWebDesktop(currentContext!)) { + selectAccountMenuItem(listAccountMenuItem.first); + } else { + selectAccountMenuItem(AccountMenuItem.none); + } + } } - @override - void onDone() {} - void selectAccountMenuItem(AccountMenuItem newAccountMenuItem) { dashBoardController.selectAccountMenuItem(newAccountMenuItem); } @@ -70,8 +61,4 @@ class ManageAccountMenuController extends BaseController { void backToMailboxDashBoard(BuildContext context) { dashBoardController.backToMailboxDashBoard(context); } - - void logout(){ - dashBoardController.logout(dashBoardController.sessionCurrent.value, dashBoardController.accountId.value); - } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/menu/manage_account_menu_view.dart b/lib/features/manage_account/presentation/menu/manage_account_menu_view.dart index 509336a136..c94b0c8878 100644 --- a/lib/features/manage_account/presentation/menu/manage_account_menu_view.dart +++ b/lib/features/manage_account/presentation/menu/manage_account_menu_view.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -28,13 +29,13 @@ class ManageAccountMenuView extends GetWidget { color: Colors.white, padding: const EdgeInsets.only(top: 16, bottom: 16, left: 16), child: Row(children: [ - (SloganBuilder(arrangedByHorizontal: true) - ..setSloganText(AppLocalizations.of(context).app_name) - ..setSloganTextAlign(TextAlign.center) - ..setSloganTextStyle(const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold)) - ..setSizeLogo(24) - ..setLogo(_imagePaths.icLogoTMail)) - .build(), + SloganBuilder( + sizeLogo: 24, + text: AppLocalizations.of(context).app_name, + textAlign: TextAlign.center, + textStyle: const TextStyle(color: Colors.black, fontSize: 20, fontWeight: FontWeight.bold), + logoSVG: _imagePaths.icTMailLogo + ), Obx(() { if (controller.dashBoardController.appInformation.value != null) { return Padding( @@ -52,63 +53,103 @@ class ManageAccountMenuView extends GetWidget { ]) ), if (!_responsiveUtils.isWebDesktop(context)) - const Divider(color: AppColor.colorDividerMailbox, height: 0.5, thickness: 0.2), + const Divider(color: AppColor.colorDividerMailbox, height: 1), Expanded(child: Container( color: _responsiveUtils.isWebDesktop(context) ? AppColor.colorBgDesktop : Colors.white, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only(left: 20, top: 24), - child: (ButtonBuilder(_imagePaths.icBack) - ..key(const Key('button_back')) - ..decoration(BoxDecoration(borderRadius: BorderRadius.circular(10), color: AppColor.colorBgMailboxSelected)) - ..paddingIcon(const EdgeInsets.only(right: 8)) - ..iconColor(AppColor.colorTextButton) - ..maxWidth(100) - ..size(16) - ..radiusSplash(10) - ..padding(const EdgeInsets.symmetric(vertical: 10)) - ..textStyle(const TextStyle(fontSize: 15, color: AppColor.colorTextButton, fontWeight: FontWeight.normal)) - ..onPressActionClick(() => controller.backToMailboxDashBoard(context)) - ..text(AppLocalizations.of(context).back, isVertical: false)) - .build()), - Padding( - padding: const EdgeInsets.only(left: 32, top: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsetsDirectional.only(start: 20, top: 24), + child: TMailButtonWidget( + key: const Key('back_to_dashboard_button'), + text: AppLocalizations.of(context).back, + icon: DirectionUtils.isDirectionRTLByLanguage(context) + ? _imagePaths.icArrowRight + : _imagePaths.icBack, + borderRadius: 10, + backgroundColor: AppColor.colorBgMailboxSelected, + iconColor: AppColor.colorTextButton, + maxWidth: 100, + padding: DirectionUtils.isDirectionRTLByLanguage(context) + ? const EdgeInsets.symmetric(vertical: 5) + : const EdgeInsets.symmetric(vertical: 10), + iconSize: DirectionUtils.isDirectionRTLByLanguage(context) ? null : 16, + textStyle: const TextStyle( + fontSize: 15, + color: AppColor.colorTextButton, + fontWeight: FontWeight.normal + ), + onTapActionCallback: () => controller.backToMailboxDashBoard(context), + ), + ), + Padding( + padding: const EdgeInsetsDirectional.only(start: 32, top: 20), child: Text( - AppLocalizations.of(context).manage_account, - style: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold, fontSize: 17))), - const SizedBox(height: 12), - Obx( - () => ListView.builder( - padding: const EdgeInsets.only(left: 16, right: 8), - key: const Key('list_manage_account_property'), - shrinkWrap: true, - itemCount: controller.listAccountMenuItem.length, - itemBuilder: (context, index) => Obx(() => AccountMenuItemTileBuilder( - context, - _imagePaths, - _responsiveUtils, - controller.listAccountMenuItem[index], - controller.dashBoardController.accountMenuItemSelected.value, - onSelectAccountMenuItemAction: (newAccountMenuItem) => - controller.selectAccountMenuItem(newAccountMenuItem)))), - ), - const Padding( - padding: EdgeInsets.symmetric(vertical: 16), - child: Divider(color: AppColor.lineItemListColor, height: 0.5, thickness: 0.2)), - Padding(padding: const EdgeInsets.only(left: 32), - child: InkWell( + AppLocalizations.of(context).manage_account, + style: const TextStyle( + color: Colors.black, + fontWeight: FontWeight.bold, + fontSize: 17 + ) + ) + ), + const SizedBox(height: 12), + Obx(() { + if (controller.listAccountMenuItem.isNotEmpty) { + return ListView.builder( + key: const Key('list_manage_account_menu_item'), + padding: const EdgeInsetsDirectional.only(start: 16, end: 8), + shrinkWrap: true, + itemCount: controller.listAccountMenuItem.length, + itemBuilder: (context, index) => Obx(() { + final menuItem = controller.listAccountMenuItem[index]; + return AccountMenuItemTileBuilder( + menuItem, + controller.dashBoardController.accountMenuItemSelected.value, + onSelectAccountMenuItemAction: controller.selectAccountMenuItem + ); + }) + ); + } else { + return const SizedBox.shrink(); + } + }), + const Padding( + padding: EdgeInsets.symmetric(vertical: 16), + child: Divider(color: AppColor.lineItemListColor, height: 1)), + Padding( + padding: const EdgeInsetsDirectional.only(start: 20, end: 10), + child: Material( + color: Colors.transparent, + child: InkWell( onTap: () { - controller.logout(); + controller.dashBoardController.logout( + controller.dashBoardController.sessionCurrent, + controller.dashBoardController.accountId.value + ); }, - child: Row(children: [ - SvgPicture.asset(_imagePaths.icSignOut, fit: BoxFit.fill), - const SizedBox(width: 12), - Expanded(child: Text(AppLocalizations.of(context).sign_out, - style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 15, color: Colors.black))) - ]) + borderRadius: BorderRadius.circular(10), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Row(children: [ + SvgPicture.asset(_imagePaths.icSignOut, fit: BoxFit.fill), + const SizedBox(width: 12), + Expanded(child: Text( + AppLocalizations.of(context).sign_out, + style: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 15, + color: Colors.black + ) + )) + ]), + ) + ), ) - ), - ]), + ), + ] + ), )), ] ) diff --git a/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart b/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart index 15a0a82a68..ccd7e5cdb9 100644 --- a/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart +++ b/lib/features/manage_account/presentation/menu/settings/settings_first_level_view.dart @@ -3,7 +3,7 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/mailbox/presentation/widgets/user_information_widget_builder.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/widgets/user_information_widget.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings/settings_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/widgets/setting_first_level_tile_builder.dart'; @@ -20,13 +20,10 @@ class SettingsFirstLevelView extends GetWidget { Widget build(BuildContext context) { return SingleChildScrollView( child: Column(children: [ - Obx(() => Padding( + Obx(() => UserInformationWidget( + userProfile: controller.manageAccountDashboardController.userProfile.value, padding: SettingsUtils.getPaddingInFirstLevel(context, _responsiveUtils), - child: UserInformationWidgetBuilder( - _imagePaths, - controller.manageAccountDashboardController.userProfile.value, - titlePadding: const EdgeInsets.only(left: 16)) - )), + titlePadding: const EdgeInsetsDirectional.only(start: 16))), Divider( color: AppColor.colorDividerComposer, height: 1, @@ -46,7 +43,7 @@ class SettingsFirstLevelView extends GetWidget { endIndent: SettingsUtils.getHorizontalPadding(context, _responsiveUtils) ), Obx(() { - if (controller.manageAccountDashboardController.checkAvailableRuleFilterInSession()) { + if (controller.manageAccountDashboardController.isRuleFilterCapabilitySupported) { return Column(children: [ SettingFirstLevelTileBuilder( AccountMenuItem.emailRules.getName(context), @@ -66,7 +63,7 @@ class SettingsFirstLevelView extends GetWidget { } }), Obx(() { - if (controller.manageAccountDashboardController.checkAvailableForwardInSession()) { + if (controller.manageAccountDashboardController.isForwardCapabilitySupported) { return Column(children: [ SettingFirstLevelTileBuilder( AccountMenuItem.forward.getName(context), @@ -86,7 +83,7 @@ class SettingsFirstLevelView extends GetWidget { } }), Obx(() { - if (controller.manageAccountDashboardController.checkAvailableVacationInSession()) { + if (controller.manageAccountDashboardController.isVacationCapabilitySupported) { return Column(children: [ SettingFirstLevelTileBuilder( AccountMenuItem.vacation.getName(context), @@ -134,7 +131,7 @@ class SettingsFirstLevelView extends GetWidget { AppLocalizations.of(context).sign_out, _imagePaths.icSignOut, () => controller.manageAccountDashboardController.logout( - controller.manageAccountDashboardController.sessionCurrent.value, + controller.manageAccountDashboardController.sessionCurrent, controller.manageAccountDashboardController.accountId.value) ), ]), diff --git a/lib/features/manage_account/presentation/menu/settings/settings_view.dart b/lib/features/manage_account/presentation/menu/settings/settings_view.dart index c49e02d3c8..bc90407a69 100644 --- a/lib/features/manage_account/presentation/menu/settings/settings_view.dart +++ b/lib/features/manage_account/presentation/menu/settings/settings_view.dart @@ -3,7 +3,8 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -21,6 +22,7 @@ import 'package:tmail_ui_user/features/manage_account/presentation/profiles/prof import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_view.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; typedef CloseSettingsViewAction = void Function(); @@ -33,57 +35,58 @@ class SettingsView extends GetWidget { @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SafeArea( - bottom: false, - child: SizedBox.fromSize( - size: const Size.fromHeight(52), - child: Padding( - padding: SettingsUtils.getPaddingAppBar(context, _responsiveUtils), - child: _buildAppbar(context))), - ), - const Divider(color: AppColor.colorDividerComposer, height: 1), - SafeArea( - bottom: false, - top: false, - child: Obx(() { - if (controller.manageAccountDashboardController.vacationResponse.value?.vacationResponderIsValid == true) { - return VacationNotificationMessageWidget( + return Container( + color: Colors.white, + child: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox.fromSize( + size: const Size.fromHeight(52), + child: Padding( + padding: SettingsUtils.getPaddingAppBar(context, _responsiveUtils), + child: _buildAppbar(context))), + const Divider(color: AppColor.colorDividerComposer, height: 1), + Obx(() { + if (controller.manageAccountDashboardController.vacationResponse.value?.vacationResponderIsValid == true) { + return VacationNotificationMessageWidget( + margin: const EdgeInsets.only( + left: PlatformInfo.isWeb ? 24 : 16, + right: PlatformInfo.isWeb ? 24 : 16, + top: 16), + fromAccountDashBoard: true, + vacationResponse: controller.manageAccountDashboardController.vacationResponse.value!, + actionGotoVacationSetting: !controller.manageAccountDashboardController.inVacationSettings() + ? () => controller.manageAccountDashboardController.selectAccountMenuItem(AccountMenuItem.vacation) + : null, + actionEndNow: () => controller.manageAccountDashboardController.disableVacationResponder()); + } else if ((controller.manageAccountDashboardController.vacationResponse.value?.vacationResponderIsWaiting == true + || controller.manageAccountDashboardController.vacationResponse.value?.vacationResponderIsStopped == true) + && controller.manageAccountDashboardController.inVacationSettings()) { + return VacationNotificationMessageWidget( margin: const EdgeInsets.only( - left: BuildUtils.isWeb ? 24 : 16, - right: BuildUtils.isWeb ? 24 : 16, - top: 16), + left: PlatformInfo.isWeb ? 24 : 16, + right: PlatformInfo.isWeb ? 24 : 16, + top: 16), fromAccountDashBoard: true, vacationResponse: controller.manageAccountDashboardController.vacationResponse.value!, - actionEndNow: () => controller.manageAccountDashboardController.disableVacationResponder()); - } else if ((controller.manageAccountDashboardController.vacationResponse.value?.vacationResponderIsWaiting == true - || controller.manageAccountDashboardController.vacationResponse.value?.vacationResponderIsStopped == true) - && controller.manageAccountDashboardController.inVacationSettings()) { - return VacationNotificationMessageWidget( - margin: const EdgeInsets.only( - left: BuildUtils.isWeb ? 24 : 16, - right: BuildUtils.isWeb ? 24 : 16, - top: 16), - fromAccountDashBoard: true, - vacationResponse: controller.manageAccountDashboardController.vacationResponse.value!, - padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), - leadingIcon: const Padding( - padding: EdgeInsets.only(right: 16), - child: Icon(Icons.timer, size: 20), - ) - ); - } else { - return const SizedBox.shrink(); - } - }) + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16), + leadingIcon: Padding( + padding: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) ? 0 : 16, + left: AppUtils.isDirectionRTL(context) ? 16 : 0, + ), + child: const Icon(Icons.timer, size: 20), + ) + ); + } else { + return const SizedBox.shrink(); + } + }), + Expanded(child: _bodySettingsScreen()) + ] ), - Expanded(child: SafeArea( - top: false, - child: _bodySettingsScreen() - )) - ] + ), ); } @@ -119,52 +122,58 @@ class SettingsView extends GetWidget { } Widget _buildSettingLevel1AppBar(BuildContext context) { - return Stack(children: [ - Align( - alignment: Alignment.centerLeft, - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: controller.backToUniversalSettings, - customBorder: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(15))), - child: Tooltip( - message: AppLocalizations.of(context).back, - child: Container( - color: Colors.transparent, - height: 40, - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - SvgPicture.asset( - _imagePaths.icBack, - width: 18, - height: 18, - color: AppColor.colorTextButton, - fit: BoxFit.fill), - Container( - margin: const EdgeInsets.only(left: 8), - constraints: const BoxConstraints(maxWidth: 100), - child: Text( - AppLocalizations.of(context).settings, - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - style: const TextStyle(fontSize: 17, color: AppColor.colorTextButton)), + return Row(children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: controller.backToUniversalSettings, + customBorder: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(15))), + child: Tooltip( + message: AppLocalizations.of(context).back, + child: Container( + color: Colors.transparent, + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + SvgPicture.asset( + DirectionUtils.isDirectionRTLByLanguage(context) + ? _imagePaths.icCollapseFolder + : _imagePaths.icBack, + colorFilter: AppColor.colorTextButton.asFilter(), + fit: BoxFit.fill), + Container( + margin: EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 0 : 4, + right: AppUtils.isDirectionRTL(context) ? 4 : 0, ), - ]), - ), + constraints: const BoxConstraints(maxWidth: 80), + child: Text( + AppLocalizations.of(context).settings, + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: const TextStyle(fontSize: 17, color: AppColor.colorTextButton) + ) + ), + ]), ), ), ), ), - Align( - alignment: Alignment.center, - child: Text( - controller.manageAccountDashboardController.accountMenuItemSelected.value.getName(context), - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - style: const TextStyle(fontSize: 20, color: Colors.black, fontWeight: FontWeight.bold))) + Expanded(child: Text( + controller.manageAccountDashboardController.accountMenuItemSelected.value.getName(context), + maxLines: 1, + textAlign: TextAlign.center, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: const TextStyle( + fontSize: 20, + color: Colors.black, + fontWeight: FontWeight.bold + ) + )), + Container(constraints: const BoxConstraints(maxWidth: 80)) ]); } @@ -194,19 +203,19 @@ class SettingsView extends GetWidget { case AccountMenuItem.languageAndRegion: return LanguageAndRegionView(); case AccountMenuItem.emailRules: - if (controller.manageAccountDashboardController.checkAvailableRuleFilterInSession()) { + if (controller.manageAccountDashboardController.isRuleFilterCapabilitySupported) { return EmailRulesView(); } else { return const SizedBox.shrink(); } case AccountMenuItem.forward: - if (controller.manageAccountDashboardController.checkAvailableForwardInSession()) { + if (controller.manageAccountDashboardController.isForwardCapabilitySupported) { return ForwardView(); } else { return const SizedBox.shrink(); } case AccountMenuItem.vacation: - if (controller.manageAccountDashboardController.checkAvailableVacationInSession()) { + if (controller.manageAccountDashboardController.isVacationCapabilitySupported) { return VacationView(); } else { return const SizedBox.shrink(); diff --git a/lib/features/manage_account/presentation/menu/settings_utils.dart b/lib/features/manage_account/presentation/menu/settings_utils.dart index 270ed460d3..6e76620bcc 100644 --- a/lib/features/manage_account/presentation/menu/settings_utils.dart +++ b/lib/features/manage_account/presentation/menu/settings_utils.dart @@ -1,7 +1,8 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; class SettingsUtils { static double getHorizontalPadding(BuildContext context, ResponsiveUtils responsiveUtils) { @@ -31,7 +32,7 @@ class SettingsUtils { } static EdgeInsets getMarginViewForSettingDetails(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (responsiveUtils.isDesktop(context)) { return const EdgeInsets.all(16); } else if (responsiveUtils.isTabletLarge(context) || @@ -107,14 +108,6 @@ class SettingsUtils { } } - static EdgeInsets getPaddingHeaderWidgetForwarding(BuildContext context, ResponsiveUtils responsiveUtils) { - if (responsiveUtils.isPortraitMobile(context)) { - return const EdgeInsets.only(left: 16, right: 16); - } else { - return const EdgeInsets.all(12); - } - } - static EdgeInsets getPaddingKeepLocalSwitchButtonForwarding(BuildContext context, ResponsiveUtils responsiveUtils) { if (responsiveUtils.isPortraitMobile(context)) { return const EdgeInsets.symmetric(horizontal: 18, vertical: 14); @@ -142,4 +135,28 @@ class SettingsUtils { return EdgeInsets.zero; } } + + static EdgeInsets getMarginSettingDetailsView(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isWebDesktop(context)) { + return const EdgeInsets.all(16); + } else { + return EdgeInsets.zero; + } + } + + static EdgeInsets getPaddingHeaderSetting(BuildContext context) { + if (AppUtils.isDirectionRTL(context)) { + return const EdgeInsets.only(top: 25, bottom: 25, right: 32); + } else { + return const EdgeInsets.only(top: 25, bottom: 25, left: 32); + } + } + + static EdgeInsets getPaddingRightHeaderSetting(BuildContext context) { + if (AppUtils.isDirectionRTL(context)) { + return const EdgeInsets.only(right: 48, top: 16, bottom: 10, left: 10); + } else { + return const EdgeInsets.only(right: 10, top: 16, bottom: 10, left: 48); + } + } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/menu/widgets/account_menu_item_tile_builder.dart b/lib/features/manage_account/presentation/menu/widgets/account_menu_item_tile_builder.dart index a48b7466bc..5f689cf609 100644 --- a/lib/features/manage_account/presentation/menu/widgets/account_menu_item_tile_builder.dart +++ b/lib/features/manage_account/presentation/menu/widgets/account_menu_item_tile_builder.dart @@ -1,61 +1,75 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/account_menu_item.dart'; typedef OnSelectAccountMenuItemAction = void Function(AccountMenuItem); class AccountMenuItemTileBuilder extends StatelessWidget { - final BuildContext _context; - final ImagePaths _imagePaths; - final ResponsiveUtils _responsiveUtils; final AccountMenuItem _menuItem; - final AccountMenuItem _menuItemSelected; + final AccountMenuItem? _menuItemSelected; final OnSelectAccountMenuItemAction? onSelectAccountMenuItemAction; const AccountMenuItemTileBuilder( - this._context, - this._imagePaths, - this._responsiveUtils, this._menuItem, this._menuItemSelected, - {Key? key, this.onSelectAccountMenuItemAction} + { + Key? key, + this.onSelectAccountMenuItemAction + } ) : super(key: key); @override Widget build(BuildContext context) { + final imagePaths = Get.find(); + return Padding( + key: const Key('account_menu_item_tile'), padding: const EdgeInsets.only(top: 6), - child: InkWell( - onTap: () => onSelectAccountMenuItemAction?.call(_menuItem), - child: Container( - key: const Key('account_menu_item_tile'), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onSelectAccountMenuItemAction?.call(_menuItem), + borderRadius: BorderRadius.circular(10), + child: Container( decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: backgroundColorItem), + borderRadius: BorderRadius.circular(10), + color: _getBackgroundColorItem(context)), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: Column(children: [ Row(children: [ - SvgPicture.asset(_menuItem.getIcon(_imagePaths), - width: 20, - height: 20, - fit: BoxFit.fill), + SvgPicture.asset( + _menuItem.getIcon(imagePaths), + width: 20, + height: 20, + fit: BoxFit.fill), const SizedBox(width: 12), - Expanded(child: Text(_menuItem.getName(context), - style: const TextStyle(fontWeight: FontWeight.normal, fontSize: 15, color: Colors.black))) + Expanded(child: Text( + _menuItem.getName(context), + style: const TextStyle( + fontWeight: FontWeight.normal, + fontSize: 15, + color: Colors.black + ) + )) ]), ]) - )), + )), + ), ); } - Color get backgroundColorItem { + Color _getBackgroundColorItem(BuildContext context) { + final responsiveUtils = Get.find(); + if (_menuItemSelected == _menuItem) { return AppColor.colorBgMailboxSelected; + } else { + return responsiveUtils.isWebDesktop(context) + ? Colors.transparent + : Colors.transparent; } - return _responsiveUtils.isWebDesktop(_context) - ? AppColor.colorBgDesktop - : Colors.white; } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/menu/widgets/setting_first_level_tile_builder.dart b/lib/features/manage_account/presentation/menu/widgets/setting_first_level_tile_builder.dart index a0b653c6b1..137fcaefbb 100644 --- a/lib/features/manage_account/presentation/menu/widgets/setting_first_level_tile_builder.dart +++ b/lib/features/manage_account/presentation/menu/widgets/setting_first_level_tile_builder.dart @@ -2,10 +2,12 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; typedef SettingFirstLevelTileClickAction = void Function(); @@ -30,57 +32,71 @@ class SettingFirstLevelTileBuilder extends StatelessWidget { @override Widget build(BuildContext context) { - return InkWell(child: Padding( - padding: const EdgeInsets.only(top: 24, bottom: 24), - child: Row( - children: [ - Expanded(flex: 9, child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + return Material( + color: Colors.transparent, + child: InkWell( + onTap: clickAction, + child: Padding( + padding: const EdgeInsetsDirectional.symmetric(vertical: 24), + child: Row( children: [ - Row( + Expanded(flex: 9, child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: EdgeInsets.only(left: SettingsUtils.getHorizontalPadding(context, _responsiveUtils)), - child: _buildSettingIcon(context)), - Expanded(child: Padding( - padding: const EdgeInsets.only(left: 12, right: 12), - child: Text( - title, - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: const TextStyle( - fontSize: 16, - color: AppColor.colorNameEmail, - fontWeight: FontWeight.w400, - ) - ) - )) - ], - ), - subtitle != null - ? Padding( - padding: EdgeInsets.only(left: _getSubtitleLeftPadding(context), right: 12, top: 12), - child: Text( - subtitle!, - style: const TextStyle( - fontSize: 13, - color: AppColor.colorContentEmail, - fontWeight: FontWeight.w400))) - : const SizedBox.shrink() - ] - )), - IconButton( - padding: EdgeInsets.only(right: SettingsUtils.getHorizontalPadding(context, _responsiveUtils)), - icon: SvgPicture.asset( - _imagePath.icCollapseFolder, - fit: BoxFit.fill, - color: AppColor.colorCollapseMailbox), - onPressed: clickAction - ) - ])), - onTap: clickAction, + Row( + children: [ + Padding( + padding: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) ? SettingsUtils.getHorizontalPadding(context, _responsiveUtils) : 0, + left: AppUtils.isDirectionRTL(context) ? 0 : SettingsUtils.getHorizontalPadding(context, _responsiveUtils), + ), + child: _buildSettingIcon(context)), + Expanded(child: Padding( + padding: const EdgeInsets.only(left: 12, right: 12), + child: Text( + title, + maxLines: 1, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + style: const TextStyle( + fontSize: 16, + color: AppColor.colorNameEmail, + fontWeight: FontWeight.w400, + ) + ) + )) + ], + ), + subtitle != null + ? Padding( + padding: EdgeInsets.only( + left: AppUtils.isDirectionRTL(context) ? 12 : _getSubtitleLeftPadding(context), + right: AppUtils.isDirectionRTL(context) ? _getSubtitleLeftPadding(context) : 12, + top: 12 + ), + child: Text( + subtitle!, + style: const TextStyle( + fontSize: 13, + color: AppColor.colorContentEmail, + fontWeight: FontWeight.w400))) + : const SizedBox.shrink() + ] + )), + IconButton( + padding: EdgeInsets.only( + right: AppUtils.isDirectionRTL(context) ? 0 : SettingsUtils.getHorizontalPadding(context, _responsiveUtils), + left: AppUtils.isDirectionRTL(context) ? SettingsUtils.getHorizontalPadding(context, _responsiveUtils) : 0, + ), + icon: SvgPicture.asset( + DirectionUtils.isDirectionRTLByLanguage(context) ? _imagePath.icBack : _imagePath.icCollapseFolder, + fit: BoxFit.fill, + colorFilter: AppColor.colorCollapseMailbox.asFilter()), + onPressed: clickAction + ) + ])), + ), ); } diff --git a/lib/features/manage_account/presentation/model/vacation/vacation_message_type.dart b/lib/features/manage_account/presentation/model/vacation/vacation_message_type.dart deleted file mode 100644 index 412a382627..0000000000 --- a/lib/features/manage_account/presentation/model/vacation/vacation_message_type.dart +++ /dev/null @@ -1,20 +0,0 @@ - -import 'package:flutter/cupertino.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -enum VacationMessageType { - plainText, - htmlTemplate -} - -extension VacationMessageTypeExtension on VacationMessageType { - - String getTitle(BuildContext context) { - switch(this) { - case VacationMessageType.plainText: - return AppLocalizations.of(context).plain_text; - case VacationMessageType.htmlTemplate: - return AppLocalizations.of(context).html_template; - } - } -} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart b/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart index 6304a31e35..982184458f 100644 --- a/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart +++ b/lib/features/manage_account/presentation/profiles/identities/identities_controller.dart @@ -1,18 +1,19 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/dialog/confirmation_dialog_builder.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_bindings.dart'; -import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_view.dart'; import 'package:tmail_ui_user/features/identity_creator/presentation/model/identity_creator_arguments.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_identity_request.dart'; import 'package:tmail_ui_user/features/manage_account/domain/model/edit_identity_request.dart'; @@ -21,17 +22,21 @@ import 'package:tmail_ui_user/features/manage_account/domain/state/create_new_id import 'package:tmail_ui_user/features/manage_account/domain/state/delete_identity_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/edit_identity_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/state/get_all_identities_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/transform_html_signature_state.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_default_identity_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_identity_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/delete_identity_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/edit_default_identity_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/edit_identity_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/transform_html_signature_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/extensions/identity_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/manage_account_dashboard_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/identity_action_type.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/model/settings_page_level.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class IdentitiesController extends BaseController { @@ -40,7 +45,6 @@ class IdentitiesController extends BaseController { final _appToast = Get.find(); final _imagePaths = Get.find(); final _responsiveUtils = Get.find(); - final _identityCreatorBindings = IdentityCreatorBindings(); final GetAllIdentitiesInteractor _getAllIdentitiesInteractor; final CreateNewIdentityInteractor _createNewIdentityInteractor; @@ -48,75 +52,65 @@ class IdentitiesController extends BaseController { final DeleteIdentityInteractor _deleteIdentityInteractor; final EditIdentityInteractor _editIdentityInteractor; final EditDefaultIdentityInteractor _editDefaultIdentityInteractor; + final TransformHtmlSignatureInteractor _transformHtmlSignatureInteractor; final identitySelected = Rxn(); + final signatureSelected = Rxn(); final listAllIdentities = [].obs; - late Worker accountIdWorker; - IdentitiesController( this._getAllIdentitiesInteractor, this._deleteIdentityInteractor, this._createNewIdentityInteractor, this._editIdentityInteractor, this._createNewDefaultIdentityInteractor, - this._editDefaultIdentityInteractor + this._editDefaultIdentityInteractor, + this._transformHtmlSignatureInteractor ); @override void onInit() { - _initWorker(); + _registerObxStreamListener(); super.onInit(); } @override - void onClose() { - _clearWorker(); - super.onClose(); + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetAllIdentitiesSuccess) { + _handleGetAllIdentitiesSuccess(success); + } else if (success is CreateNewIdentitySuccess) { + _createNewIdentitySuccess(success); + } else if (success is CreateNewDefaultIdentitySuccess) { + _createNewDefaultIdentitySuccess(success); + } else if (success is DeleteIdentitySuccess) { + _deleteIdentitySuccess(success); + } else if (success is EditIdentitySuccess) { + _editIdentitySuccess(success); + } else if (success is TransformHtmlSignatureSuccess) { + signatureSelected.value = success.signature; + } } @override - void onDone() { - viewState.value.fold( - (failure) { - if (failure is DeleteIdentityFailure) { - _deleteIdentityFailure(failure); - } - }, - (success) { - if (success is GetAllIdentitiesSuccess) { - _handleGetAllIdentitiesSuccess(success); - } else if (success is CreateNewIdentitySuccess) { - _createNewIdentitySuccess(success); - } else if (success is CreateNewDefaultIdentitySuccess) { - _createNewDefaultIdentitySuccess(success); - } else if (success is DeleteIdentitySuccess) { - _deleteIdentitySuccess(success); - } else if (success is EditIdentitySuccess) { - _editIdentitySuccess(success); - } - } - ); + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is DeleteIdentityFailure) { + _deleteIdentityFailure(failure); + } } - void _initWorker() { - accountIdWorker = ever(_accountDashBoardController.accountId, (accountId) { - if (accountId is AccountId) { - _getAllIdentities(accountId); + void _registerObxStreamListener() { + ever(_accountDashBoardController.accountId, (accountId) { + final session = _accountDashBoardController.sessionCurrent; + if (accountId != null && session != null) { + _getAllIdentities(session, accountId); } }); - - if (_accountDashBoardController.settingsPageLevel.value == SettingsPageLevel.level1) { - _accountDashBoardController.accountId.refresh(); - } - } - - void _clearWorker() { - accountIdWorker.call(); } - void _getAllIdentities(AccountId accountId) { - consumeState(_getAllIdentitiesInteractor.execute(accountId)); + void _getAllIdentities(Session session, AccountId accountId) { + consumeState(_getAllIdentitiesInteractor.execute(session, accountId)); } void _refreshAllIdentities() { @@ -124,8 +118,9 @@ class IdentitiesController extends BaseController { listAllIdentities.clear(); final accountId = _accountDashBoardController.accountId.value; - if (accountId != null) { - _getAllIdentities(accountId); + final session = _accountDashBoardController.sessionCurrent; + if (accountId != null && session != null) { + _getAllIdentities(session, accountId); } } @@ -143,76 +138,50 @@ class IdentitiesController extends BaseController { } void selectIdentity(Identity? newIdentity) { + signatureSelected.value = null; identitySelected.value = newIdentity; + + if (newIdentity != null) { + consumeState(_transformHtmlSignatureInteractor.execute(newIdentity.signatureAsString)); + } } void goToCreateNewIdentity(BuildContext context) async { final accountId = _accountDashBoardController.accountId.value; final userProfile = _accountDashBoardController.userProfile.value; - final session = _accountDashBoardController.sessionCurrent.value; + final session = _accountDashBoardController.sessionCurrent; if (accountId != null && session != null && userProfile != null) { final arguments = IdentityCreatorArguments(accountId, session, userProfile); - if (BuildUtils.isWeb) { - showDialogIdentityCreator( - context: context, - arguments: arguments, - onCreatedIdentity: (arguments) { - if (arguments is CreateNewIdentityRequest) { - _createNewIdentityAction(accountId, arguments); - } else if (arguments is EditIdentityRequest) { - _editIdentityAction(accountId, arguments); - } - }); - } else { - final newIdentityArguments = - await _getIdentityRequest(context, arguments); - - if (newIdentityArguments is CreateNewIdentityRequest) { - _createNewIdentityAction(accountId, newIdentityArguments); - } else if (newIdentityArguments is EditIdentityRequest) { - _editIdentityAction(accountId, newIdentityArguments); - } + final newIdentityArguments = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.identityCreator, arguments: arguments) + : await push(AppRoutes.identityCreator, arguments: arguments); + + if (newIdentityArguments is CreateNewIdentityRequest) { + _createNewIdentityAction(session, accountId, newIdentityArguments); + } else if (newIdentityArguments is EditIdentityRequest) { + _editIdentityAction(session, accountId, newIdentityArguments); } } } - Future _getIdentityRequest( - BuildContext context, - dynamic arguments - ) async { - return await showModalBottomSheet( - enableDrag: false, - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0)), - ), - builder: (context) { - _identityCreatorBindings.dependencies(); - return IdentityCreatorView.fromArguments(arguments); - }); - } - void _createNewIdentityAction( + Session session, AccountId accountId, CreateNewIdentityRequest identityRequest ) async { if (identityRequest.isDefaultIdentity) { - consumeState(_createNewDefaultIdentityInteractor.execute(accountId, identityRequest)); + consumeState(_createNewDefaultIdentityInteractor.execute(session, accountId, identityRequest)); } else { - consumeState(_createNewIdentityInteractor.execute(accountId, identityRequest)); + consumeState(_createNewIdentityInteractor.execute(session, accountId, identityRequest)); } } void _createNewIdentitySuccess(CreateNewIdentitySuccess success) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - message: AppLocalizations.of(currentContext!).you_have_created_a_new_identity, - icon: _imagePaths.icSelected); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).you_have_created_a_new_identity); } _refreshAllIdentities(); @@ -220,10 +189,9 @@ class IdentitiesController extends BaseController { void _createNewDefaultIdentitySuccess(CreateNewDefaultIdentitySuccess success) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( + _appToast.showToastSuccessMessage( currentOverlayContext!, - message: AppLocalizations.of(currentContext!).you_have_created_a_new_default_identity, - icon: _imagePaths.icSelected); + AppLocalizations.of(currentContext!).you_have_created_a_new_default_identity); } _refreshAllIdentities(); @@ -243,18 +211,19 @@ class IdentitiesController extends BaseController { void _deleteIdentityAction(Identity identity) { popBack(); + final session = _accountDashBoardController.sessionCurrent; final accountId = _accountDashBoardController.accountId.value; - if (accountId != null && identity.id != null) { - consumeState(_deleteIdentityInteractor.execute(accountId, identity.id!)); + if (accountId != null && session != null && identity.id != null) { + consumeState(_deleteIdentityInteractor.execute(session, accountId, identity.id!)); } } void _deleteIdentitySuccess(DeleteIdentitySuccess success) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - message: AppLocalizations.of(currentContext!).identity_has_been_deleted, - icon: _imagePaths.icDeleteToast); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).identity_has_been_deleted, + leadingSVGIcon: _imagePaths.icDeleteToast); } _refreshAllIdentities(); @@ -287,55 +256,44 @@ class IdentitiesController extends BaseController { void goToEditIdentity(BuildContext context, Identity identity) async { final accountId = _accountDashBoardController.accountId.value; final userProfile = _accountDashBoardController.userProfile.value; - final session = _accountDashBoardController.sessionCurrent.value; + final session = _accountDashBoardController.sessionCurrent; if (accountId != null && session != null && userProfile != null) { final arguments = IdentityCreatorArguments( - accountId, - session, - userProfile, - identity: identity, - actionType: IdentityActionType.edit); - - if (BuildUtils.isWeb) { - showDialogIdentityCreator( - context: context, - arguments: arguments, - onCreatedIdentity: (arguments) { - if (arguments is CreateNewIdentityRequest) { - _createNewIdentityAction(accountId, arguments); - } else if (arguments is EditIdentityRequest) { - _editIdentityAction(accountId, arguments); - } - }); - } else { - final newIdentityArguments = await _getIdentityRequest(context, arguments); - - if (newIdentityArguments is CreateNewIdentityRequest) { - _createNewIdentityAction(accountId, newIdentityArguments); - } else if (newIdentityArguments is EditIdentityRequest) { - _editIdentityAction(accountId, newIdentityArguments); - } + accountId, + session, + userProfile, + identity: identity, + actionType: IdentityActionType.edit); + + final newIdentityArguments = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.identityCreator, arguments: arguments) + : await push(AppRoutes.identityCreator, arguments: arguments); + + if (newIdentityArguments is CreateNewIdentityRequest) { + _createNewIdentityAction(session, accountId, newIdentityArguments); + } else if (newIdentityArguments is EditIdentityRequest) { + _editIdentityAction(session, accountId, newIdentityArguments); } } } void _editIdentityAction( - AccountId accountId, + Session session, + AccountId accountId, EditIdentityRequest editIdentityRequest ) async { if (editIdentityRequest.isDefaultIdentity) { - consumeState(_editDefaultIdentityInteractor.execute(accountId, editIdentityRequest)); + consumeState(_editDefaultIdentityInteractor.execute(session, accountId, editIdentityRequest)); } else { - consumeState(_editIdentityInteractor.execute(accountId, editIdentityRequest)); + consumeState(_editIdentityInteractor.execute(session, accountId, editIdentityRequest)); } } void _editIdentitySuccess(EditIdentitySuccess success) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - message: AppLocalizations.of(currentContext!).you_are_changed_your_identity_successfully, - icon: _imagePaths.icSelected); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).you_are_changed_your_identity_successfully); } _refreshAllIdentities(); diff --git a/lib/features/manage_account/presentation/profiles/identities/identities_view.dart b/lib/features/manage_account/presentation/profiles/identities/identities_view.dart index 396758c92f..75e4092afe 100644 --- a/lib/features/manage_account/presentation/profiles/identities/identities_view.dart +++ b/lib/features/manage_account/presentation/profiles/identities/identities_view.dart @@ -18,7 +18,6 @@ class IdentitiesView extends GetWidget with PopupMenuWidge @override Widget build(BuildContext context) { return Container( - color: Colors.transparent, margin: const EdgeInsets.all(24), child: _responsiveUtils.isWebDesktop(context) ? _buildIdentitiesViewWebDesktop(context) @@ -28,10 +27,9 @@ class IdentitiesView extends GetWidget with PopupMenuWidge Widget _buildIdentitiesViewMobile(BuildContext context) { return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ IdentitiesHeaderWidget( - imagePaths: _imagePaths, - responsiveUtils: _responsiveUtils, onAddNewIdentityAction: () => controller.goToCreateNewIdentity(context), ), const SizedBox(height: 12), @@ -51,8 +49,6 @@ class IdentitiesView extends GetWidget with PopupMenuWidge SizedBox( width: 224, child: IdentitiesHeaderWidget( - imagePaths: _imagePaths, - responsiveUtils: _responsiveUtils, onAddNewIdentityAction: () => controller.goToCreateNewIdentity(context), ) ), diff --git a/lib/features/manage_account/presentation/profiles/identities/identity_bindings.dart b/lib/features/manage_account/presentation/profiles/identities/identity_bindings.dart index 6cfc2de22f..cf1f63c4c4 100644 --- a/lib/features/manage_account/presentation/profiles/identities/identity_bindings.dart +++ b/lib/features/manage_account/presentation/profiles/identities/identity_bindings.dart @@ -5,6 +5,7 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/delete_ide import 'package:tmail_ui_user/features/manage_account/domain/usecases/edit_default_identity_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/edit_identity_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/transform_html_signature_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identities_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identity_interactors_bindings.dart'; @@ -14,13 +15,14 @@ class IdentityBindings extends Bindings { void dependencies() { IdentityInteractorsBindings().dependencies(); - Get.lazyPut(() => IdentitiesController( + Get.put(IdentitiesController( Get.find(), Get.find(), Get.find(), Get.find(), Get.find(), - Get.find() + Get.find(), + Get.find() )); } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/profiles/identities/identity_interactors_bindings.dart b/lib/features/manage_account/presentation/profiles/identities/identity_interactors_bindings.dart index 36586c7606..495d435b3f 100644 --- a/lib/features/manage_account/presentation/profiles/identities/identity_interactors_bindings.dart +++ b/lib/features/manage_account/presentation/profiles/identities/identity_interactors_bindings.dart @@ -1,3 +1,4 @@ +import 'package:core/presentation/utils/html_transformer/html_transform.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/data/datasource/identity_data_source.dart'; @@ -11,6 +12,7 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/delete_ide import 'package:tmail_ui_user/features/manage_account/domain/usecases/edit_default_identity_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/edit_identity_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_identities_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/transform_html_signature_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/utils/identity_utils.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; @@ -34,6 +36,7 @@ class IdentityInteractorsBindings extends InteractorsBindings { @override void bindingsDataSourceImpl() { Get.lazyPut(() => IdentityDataSourceImpl( + Get.find(), Get.find(), Get.find())); } @@ -52,6 +55,7 @@ class IdentityInteractorsBindings extends InteractorsBindings { Get.lazyPut(() => EditDefaultIdentityInteractor( Get.find(), Get.find())); + Get.lazyPut(() => TransformHtmlSignatureInteractor(Get.find())); } @override diff --git a/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart b/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart index ae839f936f..a6e6e9fe34 100644 --- a/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart +++ b/lib/features/manage_account/presentation/profiles/identities/widgets/delete_identity_dialog_builder.dart @@ -25,8 +25,8 @@ class DeleteIdentityDialogBuilder extends StatelessWidget { final deleteDialogBuilder = ResponsiveWidget( responsiveUtils: responsiveUtils, mobile: (_buildDeleteDialog(context) - ..aligment(Alignment.bottomCenter) - ..outsideDialogPadding(const EdgeInsets.only(left: 0, right: 0, bottom: BuildUtils.isWeb ? 42 : 16)) + ..alignment(Alignment.bottomCenter) + ..outsideDialogPadding(const EdgeInsets.only(left: 0, right: 0, bottom: PlatformInfo.isWeb ? 42 : 16)) ..widthDialog(MediaQuery.of(context).size.width - 16) ..heightDialog(280)) .build(), @@ -36,7 +36,7 @@ class DeleteIdentityDialogBuilder extends StatelessWidget { landscapeTablet: _buildDeleteDialog(context).build(), desktop: _buildDeleteDialog(context).build()); - return BuildUtils.isWeb + return PlatformInfo.isWeb ? PointerInterceptor(child: deleteDialogBuilder) : deleteDialogBuilder; } diff --git a/lib/features/manage_account/presentation/profiles/identities/widgets/identities_header_widget.dart b/lib/features/manage_account/presentation/profiles/identities/widgets/identities_header_widget.dart index 6d581de4c4..2173a705ca 100644 --- a/lib/features/manage_account/presentation/profiles/identities/widgets/identities_header_widget.dart +++ b/lib/features/manage_account/presentation/profiles/identities/widgets/identities_header_widget.dart @@ -1,8 +1,8 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/presentation/views/button/button_builder.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/widget/material_text_icon_button.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnAddNewIdentityAction = Function(); @@ -11,20 +11,18 @@ class IdentitiesHeaderWidget extends StatelessWidget { const IdentitiesHeaderWidget({ Key? key, - required this.imagePaths, - required this.responsiveUtils, - this.onAddNewIdentityAction, + required this.onAddNewIdentityAction, }) : super(key: key); - final ImagePaths imagePaths; - final ResponsiveUtils responsiveUtils; - final OnAddNewIdentityAction? onAddNewIdentityAction; + final OnAddNewIdentityAction onAddNewIdentityAction; @override Widget build(BuildContext context) { - return Container( - color: Colors.transparent, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + final imagePaths = Get.find(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( AppLocalizations.of(context).identities, style: const TextStyle( @@ -39,24 +37,15 @@ class IdentitiesHeaderWidget extends StatelessWidget { fontWeight: FontWeight.normal, color: AppColor.colorSettingExplanation)), const SizedBox(height: 24), - (ButtonBuilder(imagePaths.icAddIdentity) - ..key(const Key('button_add_identity')) - ..decoration(BoxDecoration( - borderRadius: BorderRadius.circular(12), - color: AppColor.colorCreateNewIdentityButton)) - ..paddingIcon(const EdgeInsets.only(right: 8)) - ..iconColor(AppColor.colorTextButton) - ..size(28) - ..radiusSplash(12) - ..padding(const EdgeInsets.symmetric(vertical: 10)) - ..textStyle(const TextStyle( - fontSize: 16, - color: AppColor.colorTextButton, - fontWeight: FontWeight.w500)) - ..onPressActionClick(() => onAddNewIdentityAction?.call()) - ..text(AppLocalizations.of(context).createNewIdentity, isVertical: false) - ).build() - ]), + MaterialTextIconButton( + key: const Key('button_add_identity'), + label: AppLocalizations.of(context).createNewIdentity, + icon: imagePaths.icAddIdentity, + iconSize: 28, + minimumSize: const Size(double.infinity, 44), + onTap: onAddNewIdentityAction + ) + ] ); } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/profiles/identities/widgets/identities_radio_list_builder.dart b/lib/features/manage_account/presentation/profiles/identities/widgets/identities_radio_list_builder.dart index daba4d0543..7801b6a5bb 100644 --- a/lib/features/manage_account/presentation/profiles/identities/widgets/identities_radio_list_builder.dart +++ b/lib/features/manage_account/presentation/profiles/identities/widgets/identities_radio_list_builder.dart @@ -2,11 +2,13 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:fading_edge_scrollview/fading_edge_scrollview.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get_state_manager/get_state_manager.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/transform_html_signature_state.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identities_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/identity_list_tile_builder.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/signature_of_identity_builder.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/signature_builder.dart'; class IdentitiesRadioListBuilder extends StatelessWidget { @@ -46,7 +48,13 @@ class IdentitiesRadioListBuilder extends StatelessWidget { ...[ _buildListIdentityView(context), Container(height: 1, color: AppColor.attachmentFileBorderColor), - Obx(() => SignatureOfIdentityBuilder(identity: controller.identitySelected.value!)) + Obx(() { + if (controller.isSignatureShow) { + return SignatureBuilder(controller.signatureSelected.value!); + } else { + return _buildLoadingView(); + } + }) ] else _buildListIdentityView(context) @@ -62,9 +70,13 @@ class IdentitiesRadioListBuilder extends StatelessWidget { ...[ _buildListIdentityView(context), Container(width: 1, color: AppColor.attachmentFileBorderColor), - Expanded( - child: Obx(() => SignatureOfIdentityBuilder(identity: controller.identitySelected.value!)), - ) + Expanded(child: Obx(() { + if (controller.signatureSelected.value != null) { + return SignatureBuilder(controller.signatureSelected.value!); + } else { + return _buildLoadingView(); + } + })) ] else Expanded(child: _buildListIdentityView(context)) @@ -102,4 +114,26 @@ class IdentitiesRadioListBuilder extends StatelessWidget { )) ); } + + Widget _buildLoadingView() { + return Obx(() => controller.viewState.value.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is TransformHtmlSignatureLoading) { + return const Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.all(16), + child: SizedBox( + width: 30, + height: 30, + child: CupertinoActivityIndicator(color: AppColor.colorLoading) + )) + ); + } else { + return const SizedBox.shrink(); + } + } + )); + } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/profiles/identities/widgets/identity_list_tile_builder.dart b/lib/features/manage_account/presentation/profiles/identities/widgets/identity_list_tile_builder.dart index 94412fea81..0d7183ca29 100644 --- a/lib/features/manage_account/presentation/profiles/identities/widgets/identity_list_tile_builder.dart +++ b/lib/features/manage_account/presentation/profiles/identities/widgets/identity_list_tile_builder.dart @@ -1,7 +1,7 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; -import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/presentation/views/text/text_overflow_builder.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:jmap_dart_client/jmap/identities/identity.dart'; @@ -64,10 +64,8 @@ class IdentityListTileBuilder extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.only(bottom: 6), - child: Text( - identity.name ?? '', - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, + child: TextOverflowBuilder( + (identity.name ?? ''), style: const TextStyle( fontWeight: FontWeight.w500, fontSize: 16, @@ -121,15 +119,13 @@ class IdentityListTileBuilder extends StatelessWidget { alignment: Alignment.centerLeft, child: SvgPicture.asset(imagePath, width: 15, height: 15))), const SizedBox(width: 4), - Expanded(child: Text( - text ?? '', + Expanded(child: TextOverflowBuilder( + (text ?? ''), style: const TextStyle( color: AppColor.colorEmailAddressFull, fontWeight: FontWeight.normal, fontSize: 13, ), - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap )) ]), ); @@ -150,14 +146,13 @@ class IdentityListTileBuilder extends StatelessWidget { decoration: TextDecoration.underline, color: AppColor.colorTextButton))), const SizedBox(width: 4), - Expanded(child: Text(text ?? '', + Expanded(child: TextOverflowBuilder( + (text ?? ''), style: const TextStyle( color: AppColor.colorEmailAddressFull, fontWeight: FontWeight.normal, fontSize: 13, ), - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap )) ]), ); diff --git a/lib/features/manage_account/presentation/profiles/identities/widgets/signature_builder.dart b/lib/features/manage_account/presentation/profiles/identities/widgets/signature_builder.dart index 04bec652e3..30dcf1d1ec 100644 --- a/lib/features/manage_account/presentation/profiles/identities/widgets/signature_builder.dart +++ b/lib/features/manage_account/presentation/profiles/identities/widgets/signature_builder.dart @@ -1,68 +1,59 @@ import 'package:core/presentation/views/html_viewer/html_content_viewer_on_web_widget.dart'; import 'package:core/presentation/views/html_viewer/html_content_viewer_widget.dart'; import 'package:core/presentation/views/html_viewer/html_viewer_controller_for_web.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; -import 'package:html_unescape/html_unescape_small.dart'; -import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; class SignatureBuilder extends StatelessWidget { - const SignatureBuilder({ + const SignatureBuilder( + this.signatureSelected, { Key? key, this.width, - this.height, - this.htmlSignature, - this.textSignature, + this.height = 256 }) : super(key: key); - final Signature? htmlSignature; - final Signature? textSignature; + final String signatureSelected; final double? width; - final double? height; + final double height; @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { final signatureWidth = width ?? constraints.biggest.width; - final signatureHeight = height ?? constraints.biggest.height; + final signatureHeight = height; return Container( width: signatureWidth, + color: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0), - decoration: const BoxDecoration( - color: Colors.white, - ), - child: _buildSignature(signatureWidth, signatureHeight), + child: _buildSignature(context, signatureWidth, signatureHeight), ); } ); } - Widget _buildSignature(double width, double height) { - Widget signature; - if (htmlSignature != null && htmlSignature!.value.isNotEmpty) { - final htmlSignatureDecoded = _decodeHtml(htmlSignature!.value); - if (BuildUtils.isWeb) { - signature = HtmlContentViewerOnWeb( - contentHtml: htmlSignatureDecoded, - widthContent: width, - heightContent: height, + Widget _buildSignature(BuildContext context, double width, double height) { + if (signatureSelected.isNotEmpty) { + if (PlatformInfo.isWeb) { + return HtmlContentViewerOnWeb( + contentHtml: signatureSelected, + widthContent: width, + heightContent: height, controller: HtmlViewerControllerForWeb(), - allowResizeToDocumentSize: false); + allowResizeToDocumentSize: false, + direction: AppUtils.getCurrentDirection(context), + ); } else { - signature = HtmlContentViewer(contentHtml: htmlSignatureDecoded, heightContent: height); + return HtmlContentViewer( + contentHtml: signatureSelected, + heightContent: height, + direction: AppUtils.getCurrentDirection(context), + ); } - } else if (textSignature != null) { - signature = Text(textSignature!.value); } else { - signature = SizedBox.fromSize(size: Size(width, height)); + return SizedBox(width: width, height: height); } - return signature; - } - - String _decodeHtml(String htmlString) { - final unescape = HtmlUnescape(); - return unescape.convert(htmlString); } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/profiles/identities/widgets/signature_of_identity_builder.dart b/lib/features/manage_account/presentation/profiles/identities/widgets/signature_of_identity_builder.dart deleted file mode 100644 index c7bc148a8f..0000000000 --- a/lib/features/manage_account/presentation/profiles/identities/widgets/signature_of_identity_builder.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:jmap_dart_client/jmap/identities/identity.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/widgets/signature_builder.dart'; - -class SignatureOfIdentityBuilder extends StatelessWidget { - - const SignatureOfIdentityBuilder({Key? key, required this.identity}) : super(key: key); - - final Identity identity; - - @override - Widget build(BuildContext context) { - return SignatureBuilder( - height: 256, - htmlSignature: identity.htmlSignature, - textSignature: identity.textSignature - ); - } -} - diff --git a/lib/features/manage_account/presentation/profiles/profiles_bindings.dart b/lib/features/manage_account/presentation/profiles/profiles_bindings.dart index c180c9e453..e2b4f5477e 100644 --- a/lib/features/manage_account/presentation/profiles/profiles_bindings.dart +++ b/lib/features/manage_account/presentation/profiles/profiles_bindings.dart @@ -1,38 +1,10 @@ import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identity_bindings.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/profiles/profiles_controller.dart'; -class ProfileBindings extends BaseBindings { +class ProfileBindings extends Bindings { @override void dependencies() { - super.dependencies(); IdentityBindings().dependencies(); } - - @override - void bindingsController() { - Get.lazyPut(() => ProfilesController()); - } - - @override - void bindingsDataSource() { - } - - @override - void bindingsDataSourceImpl() { - } - - @override - void bindingsInteractor() { - } - - @override - void bindingsRepository() { - } - - @override - void bindingsRepositoryImpl() { - } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/profiles/profiles_controller.dart b/lib/features/manage_account/presentation/profiles/profiles_controller.dart deleted file mode 100644 index e7c4436db8..0000000000 --- a/lib/features/manage_account/presentation/profiles/profiles_controller.dart +++ /dev/null @@ -1,8 +0,0 @@ - -import 'package:tmail_ui_user/features/base/base_controller.dart'; - -class ProfilesController extends BaseController { - - @override - void onDone() {} -} \ No newline at end of file diff --git a/lib/features/manage_account/presentation/profiles/profiles_view.dart b/lib/features/manage_account/presentation/profiles/profiles_view.dart index a81f8512d4..b743916ca1 100644 --- a/lib/features/manage_account/presentation/profiles/profiles_view.dart +++ b/lib/features/manage_account/presentation/profiles/profiles_view.dart @@ -4,12 +4,11 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/base/setting_detail_view_builder.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/identities_view.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/profiles/profiles_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/widgets/profiles_header_widget.dart'; -class ProfilesView extends GetWidget { +class ProfilesView extends StatelessWidget { final _responsiveUtils = Get.find(); final _imagePaths = Get.find(); @@ -18,33 +17,27 @@ class ProfilesView extends GetWidget { @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: SettingsUtils.getBackgroundColor(context, _responsiveUtils), - body: Container( - width: double.infinity, - height: double.infinity, - color: SettingsUtils.getContentBackgroundColor(context, _responsiveUtils), - decoration: SettingsUtils.getBoxDecorationForContent(context, _responsiveUtils), - margin: SettingsUtils.getMarginViewForForwardSettingDetails(context, _responsiveUtils), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (_responsiveUtils.isWebDesktop(context)) - ...[ - ProfilesHeaderWidget(imagePaths: _imagePaths, responsiveUtils: _responsiveUtils), - Container(height: 1, color: AppColor.colorDividerHeaderSetting) - ], - Expanded(child: SingleChildScrollView( - physics: const ClampingScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - IdentitiesView() - ]) - )) - ], - ), - ), + return SettingDetailViewBuilder( + responsiveUtils: _responsiveUtils, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_responsiveUtils.isWebDesktop(context)) + ...[ + ProfilesHeaderWidget(imagePaths: _imagePaths, responsiveUtils: _responsiveUtils), + Container(height: 1, color: AppColor.colorDividerHeaderSetting) + ], + Expanded(child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IdentitiesView() + ] + ) + )) + ] + ) ); } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/vacation/utils/vacation_utils.dart b/lib/features/manage_account/presentation/vacation/utils/vacation_utils.dart index ef23072a70..cb28686c04 100644 --- a/lib/features/manage_account/presentation/vacation/utils/vacation_utils.dart +++ b/lib/features/manage_account/presentation/vacation/utils/vacation_utils.dart @@ -1,4 +1,17 @@ +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/material.dart'; + class VacationUtils { static const String vacationTagName = 'Vacation'; + + static EdgeInsets getPaddingView(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isDesktop(context)) { + return const EdgeInsets.symmetric(vertical: 24, horizontal: 20); + } else if (responsiveUtils.isTabletLarge(context) || responsiveUtils.isTablet(context)) { + return const EdgeInsets.symmetric(horizontal: 28, vertical: 24); + } else { + return const EdgeInsets.symmetric(horizontal: 12, vertical: 24); + } + } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/vacation/vacation_controller.dart b/lib/features/manage_account/presentation/vacation/vacation_controller.dart index ee805e5fce..a19ebc27c3 100644 --- a/lib/features/manage_account/presentation/vacation/vacation_controller.dart +++ b/lib/features/manage_account/presentation/vacation/vacation_controller.dart @@ -2,6 +2,7 @@ import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/vacation/vacation_response.dart'; +import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:rich_text_composer/richtext_controller.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; @@ -17,11 +18,8 @@ import 'package:tmail_ui_user/features/manage_account/presentation/extensions/va import 'package:tmail_ui_user/features/manage_account/presentation/manage_account_dashboard_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings/settings_controller.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/date_type.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/vacation_message_type.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/vacation_presentation.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/vacation_responder_status.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/vacation/utils/vacation_utils.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_controller_bindings.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -29,20 +27,18 @@ class VacationController extends BaseController { final _accountDashBoardController = Get.find(); final _appToast = Get.find(); - final _imagePaths = Get.find(); final _settingController = Get.find(); - final _richTextControllerForWeb = Get.find(tag: VacationUtils.vacationTagName); final GetAllVacationInteractor _getAllVacationInteractor; final UpdateVacationInteractor _updateVacationInteractor; final VerifyNameInteractor _verifyNameInteractor; + final RichTextWebController _richTextControllerForWeb; final vacationPresentation = VacationPresentation.initialize().obs; final errorMessageBody = Rxn(); - final vacationMessageType = Rx(VacationMessageType.plainText); - final messageTextController = TextEditingController(); final subjectTextController = TextEditingController(); + final subjectTextFocusNode = FocusNode(); final richTextControllerForMobile = RichTextController(); final htmlEditorMinHeight = 150; @@ -51,14 +47,13 @@ class VacationController extends BaseController { VacationResponse? currentVacation; String? _vacationMessageHtmlText; - late Worker vacationWorker; - final ScrollController scrollController = ScrollController(); VacationController( this._getAllVacationInteractor, this._updateVacationInteractor, - this._verifyNameInteractor + this._verifyNameInteractor, + this._richTextControllerForWeb ); String? get vacationMessageHtmlText => _vacationMessageHtmlText; @@ -78,21 +73,17 @@ class VacationController extends BaseController { } @override - void onDone() { - viewState.value.fold( - (failure) => null, - (success) { - if (success is GetAllVacationSuccess) { - _handleGetAllVacationSuccess(success); - } else if (success is UpdateVacationSuccess) { - _handleUpdateVacationSuccess(success); - } - } - ); + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetAllVacationSuccess) { + _handleGetAllVacationSuccess(success); + } else if (success is UpdateVacationSuccess) { + _handleUpdateVacationSuccess(success); + } } void _initWorker() { - vacationWorker = ever(_accountDashBoardController.vacationResponse, (vacation) { + ever(_accountDashBoardController.vacationResponse, (vacation) { if (vacation is VacationResponse) { currentVacation = vacation; final newVacationPresentation = currentVacation?.toVacationPresentation(); @@ -122,10 +113,9 @@ class VacationController extends BaseController { void _initializeValueForVacation(VacationPresentation newVacation) { vacationPresentation.value = newVacation; - messageTextController.text = newVacation.messagePlainText ?? ''; subjectTextController.text = newVacation.subject ?? ''; - updateMessageHtmlText(newVacation.messageHtmlText ?? ''); - if (BuildUtils.isWeb) { + updateMessageHtmlText(newVacation.messageHtmlText ?? newVacation.messagePlainText ?? ''); + if (PlatformInfo.isWeb) { _richTextControllerForWeb.editorController.setText(newVacation.messageHtmlText ?? ''); } else { richTextControllerForMobile.htmlEditorApi?.setText(newVacation.messageHtmlText ?? ''); @@ -179,7 +169,7 @@ class VacationController extends BaseController { onPrimary: Colors.white, onSurface: Colors.black), textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom(primary: AppColor.primaryColor))), + style: TextButton.styleFrom(foregroundColor: AppColor.primaryColor))), child: child!); } ); @@ -200,17 +190,19 @@ class VacationController extends BaseController { context: context, initialTime: currentTime ?? TimeOfDay.now(), builder: (context, child) { - return Theme( - data: Theme.of(context).copyWith( - colorScheme: const ColorScheme.light( - primary: AppColor.primaryColor, - onPrimary: Colors.white, - onSurface: Colors.black), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom(primary: AppColor.primaryColor))), - child: MediaQuery( - data: const MediaQueryData(alwaysUse24HourFormat: false), - child: child!), + return PointerInterceptor( + child: Theme( + data: Theme.of(context).copyWith( + colorScheme: const ColorScheme.light( + primary: AppColor.primaryColor, + onPrimary: Colors.white, + onSurface: Colors.black), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom(foregroundColor: AppColor.primaryColor))), + child: MediaQuery( + data: const MediaQueryData(alwaysUse24HourFormat: false), + child: child!), + ), ); } ); @@ -244,47 +236,46 @@ class VacationController extends BaseController { } void saveVacation(BuildContext context) async { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); if (vacationPresentation.value.isEnabled) { final fromDate = vacationPresentation.value.fromDate; if (fromDate == null) { - _appToast.showToastWithIcon( - context, - bgColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - message: AppLocalizations.of(context).errorMessageWhenStartDateVacationIsEmpty); + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).errorMessageWhenStartDateVacationIsEmpty); + } return; } final vacationStopEnabled = vacationPresentation.value.vacationStopEnabled; final toDate = vacationPresentation.value.toDate; if (vacationStopEnabled && toDate != null && toDate.isBefore(fromDate)) { - _appToast.showToastWithIcon( - context, - bgColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - message: AppLocalizations.of(context).errorMessageWhenEndDateVacationIsInValid); + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).errorMessageWhenEndDateVacationIsInValid); + } return; } - final messagePlainText = messageTextController.text; - final messageHtmlText = (BuildUtils.isWeb ? _vacationMessageHtmlText : await _getMessageHtmlText()) ?? ''; - if (messagePlainText.isEmpty && messageHtmlText.isEmpty) { - _appToast.showToastWithIcon( - context, - bgColor: AppColor.toastErrorBackgroundColor, - textColor: Colors.white, - message: AppLocalizations.of(context).errorMessageWhenMessageVacationIsEmpty); + final messageHtmlText = (PlatformInfo.isWeb ? _vacationMessageHtmlText : await _getMessageHtmlText()) ?? ''; + if (messageHtmlText.isEmpty && context.mounted) { + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).errorMessageWhenMessageVacationIsEmpty); + } return; } final subjectVacation = subjectTextController.text; final newVacationPresentation = vacationPresentation.value.copyWidth( - messagePlainText: messagePlainText, - messageHtmlText: messageHtmlText, - subject: subjectVacation); + messageHtmlText: messageHtmlText, + subject: subjectVacation + ); log('VacationController::saveVacation(): newVacationPresentation: $newVacationPresentation'); final newVacationResponse = newVacationPresentation.toVacationResponse(); log('VacationController::saveVacation(): newVacationResponse: $newVacationResponse'); @@ -307,11 +298,10 @@ class VacationController extends BaseController { void _handleUpdateVacationSuccess(UpdateVacationSuccess success) { if (success.listVacationResponse.isNotEmpty) { - if (currentContext != null && currentOverlayContext != null) { - _appToast.showToastWithIcon( - currentOverlayContext!, - message: AppLocalizations.of(currentContext!).vacationSettingSaved, - icon: _imagePaths.icChecked); + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).vacationSettingSaved); } currentVacation = success.listVacationResponse.first; log('VacationController::_handleUpdateVacationSuccess(): $currentVacation'); @@ -328,31 +318,19 @@ class VacationController extends BaseController { void updateMessageHtmlText(String? text) => _vacationMessageHtmlText = text; Future? _getMessageHtmlText() { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return _richTextControllerForWeb.editorController.getText(); } else { return richTextControllerForMobile.htmlEditorApi?.getText(); } } - void selectVacationMessageType(BuildContext context, VacationMessageType newMessageType) { - if (newMessageType == VacationMessageType.plainText && !BuildUtils.isWeb) { - _storeMessageHtmlTextOnMobile(); - } - clearFocusEditor(context); - vacationMessageType.value = newMessageType; - } - - void _storeMessageHtmlTextOnMobile() async { - final messageHtml = await _getMessageHtmlText(); - updateMessageHtmlText(messageHtml); - } - void clearFocusEditor(BuildContext context) { - if (!BuildUtils.isWeb) { + if (PlatformInfo.isMobile) { richTextControllerForMobile.htmlEditorApi?.unfocus(); + KeyboardUtils.hideSystemKeyboardMobile(); } - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); } void backToUniversalSettings(BuildContext context) { @@ -361,6 +339,8 @@ class VacationController extends BaseController { } void onFocusHTMLEditor() async { + subjectTextFocusNode.unfocus(); + await Scrollable.ensureVisible(htmlKey.currentContext!); await Future.delayed(const Duration(milliseconds: 500), () { scrollController.animateTo( @@ -383,12 +363,10 @@ class VacationController extends BaseController { @override void onClose() { - messageTextController.dispose(); + subjectTextFocusNode.dispose(); subjectTextController.dispose(); richTextControllerForMobile.dispose(); - vacationWorker.dispose(); scrollController.dispose(); - VacationControllerBindings().dispose(); super.onClose(); } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/vacation/vacation_controller_bindings.dart b/lib/features/manage_account/presentation/vacation/vacation_controller_bindings.dart index 1288fd15a6..9721a3efd0 100644 --- a/lib/features/manage_account/presentation/vacation/vacation_controller_bindings.dart +++ b/lib/features/manage_account/presentation/vacation/vacation_controller_bindings.dart @@ -1,5 +1,4 @@ import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/base/base_bindings.dart'; import 'package:tmail_ui_user/features/composer/presentation/controller/rich_text_web_controller.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/domain/usecases/get_all_vacation_interactor.dart'; @@ -7,39 +6,17 @@ import 'package:tmail_ui_user/features/manage_account/domain/usecases/update_vac import 'package:tmail_ui_user/features/manage_account/presentation/vacation/utils/vacation_utils.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_controller.dart'; -class VacationControllerBindings extends BaseBindings { +class VacationControllerBindings extends Bindings { @override - void bindingsController() { - Get.lazyPut(() => RichTextWebController(), tag: VacationUtils.vacationTagName); - Get.lazyPut(() => VacationController( - Get.find(), - Get.find(), - Get.find())); - } - - @override - void bindingsDataSource() { - } - - @override - void bindingsDataSourceImpl() { - } - - @override - void bindingsInteractor() { + void dependencies() { Get.lazyPut(() => VerifyNameInteractor()); - } - - @override - void bindingsRepository() { - } - - @override - void bindingsRepositoryImpl() { - } - - void dispose() { - Get.delete(tag: VacationUtils.vacationTagName); + Get.lazyPut(() => RichTextWebController(), tag: VacationUtils.vacationTagName); + Get.put(VacationController( + Get.find(), + Get.find(), + Get.find(), + Get.find(tag: VacationUtils.vacationTagName), + )); } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/vacation/vacation_view.dart b/lib/features/manage_account/presentation/vacation/vacation_view.dart index b80ed086d2..c643557580 100644 --- a/lib/features/manage_account/presentation/vacation/vacation_view.dart +++ b/lib/features/manage_account/presentation/vacation/vacation_view.dart @@ -1,6 +1,6 @@ import 'package:core/core.dart'; -import 'package:enough_html_editor/enough_html_editor.dart' as html_editor_mobile; +import 'package:core/presentation/utils/html_transformer/html_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -8,17 +8,17 @@ import 'package:pointer_interceptor/pointer_interceptor.dart'; import 'package:rich_text_composer/rich_text_composer.dart'; import 'package:rich_text_composer/views/widgets/rich_text_keyboard_toolbar.dart'; import 'package:tmail_ui_user/features/base/widget/border_button_field.dart'; -import 'package:tmail_ui_user/features/base/widget/text_input_decoration_builder.dart'; import 'package:tmail_ui_user/features/base/widget/text_input_field_builder.dart'; import 'package:tmail_ui_user/features/composer/presentation/mixin/rich_text_button_mixin.dart'; import 'package:tmail_ui_user/features/composer/presentation/model/button_layout_type.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/menu/settings_utils.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/base/setting_detail_view_builder.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/date_type.dart'; -import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/vacation_message_type.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/model/vacation/vacation_responder_status.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/vacation/utils/vacation_utils.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_controller.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:html_editor_enhanced/html_editor.dart' as html_editor_browser; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; class VacationView extends GetWidget with RichTextButtonMixin { @@ -29,266 +29,259 @@ class VacationView extends GetWidget with RichTextButtonMixi @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: _responsiveUtils.isWebDesktop(context) - ? AppColor.colorBgDesktop - : Colors.white, - body: GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: KeyboardRichText( + if (PlatformInfo.isWeb) { + return _buildVacationFormView(context); + } else { + return KeyboardRichText( + richTextController: controller.richTextControllerForMobile, + keyBroadToolbar: RichTextKeyboardToolBar( + titleBack: AppLocalizations.of(context).format, + backgroundKeyboardToolBarColor: PlatformInfo.isIOS + ? AppColor.colorBackgroundKeyboard + : AppColor.colorBackgroundKeyboardAndroid, + titleFormatBottomSheet: AppLocalizations.of(context).format, richTextController: controller.richTextControllerForMobile, - keyBroadToolbar: RichTextKeyboardToolBar( - titleBack: AppLocalizations.of(context).format, - backgroundKeyboardToolBarColor: AppColor.colorBackgroundKeyboard, - titleFormatBottomSheet: AppLocalizations.of(context).format, - richTextController: controller.richTextControllerForMobile, - titleQuickStyleBottomSheet: AppLocalizations.of(context).quickStyles, - titleBackgroundBottomSheet: AppLocalizations.of(context).background, - titleForegroundBottomSheet: AppLocalizations.of(context).foreground, - ), - child: Container( - width: double.infinity, - height: double.infinity, - margin: _responsiveUtils.isWebDesktop(context) - ? const EdgeInsets.all(24) - : EdgeInsets.zero, - decoration: _responsiveUtils.isWebDesktop(context) - ? BoxDecoration( - borderRadius: BorderRadius.circular(20), - border: Border.all(color: AppColor.colorBorderBodyThread, width: 1), - color: Colors.white) - : null, - padding: SettingsUtils.getMarginViewForSettingDetails(context, _responsiveUtils), - child: SingleChildScrollView( - physics: const ClampingScrollPhysics(), - controller: controller.scrollController, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (_responsiveUtils.isWebDesktop(context)) - ...[ - Text(AppLocalizations.of(context).vacation, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Colors.black)), - const SizedBox(height: 8) - ], - Text(AppLocalizations.of(context).vacationSettingExplanation, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - color: AppColor.colorVacationSettingExplanation)), - const SizedBox(height: 24), - Row(children: [ - Obx(() { - return InkWell( - onTap: () { - final newStatus = controller.isVacationDeactivated - ? VacationResponderStatus.activated - : VacationResponderStatus.deactivated; - controller.updateVacationPresentation(newStatus: newStatus); - }, - child: SvgPicture.asset( - controller.isVacationDeactivated - ? _imagePaths.icSwitchOff - : _imagePaths.icSwitchOn, - fit: BoxFit.fill, - width: 24, - height: 24) - ); - }), - const SizedBox(width: 16), - Expanded( - child: Text(AppLocalizations.of(context).vacationSettingToggleButtonAutoReply, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - color: Colors.black) - ), - ) - ]), - const SizedBox(height: 28), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - child: Column(children: [ - Obx(() => AbsorbPointer( - absorbing: controller.isVacationDeactivated, - child: Opacity( - opacity: controller.isVacationDeactivated ? 0.3 : 1.0, - child: _responsiveUtils.isPortraitMobile(context) - ? Column(children: [ - BorderButtonField( - label: AppLocalizations.of(context).startDate, - value: controller.vacationPresentation.value.startDate, - mouseCursor: SystemMouseCursors.text, - backgroundColor: AppColor.colorBackgroundVacationSettingField, - isEmpty: !controller.isVacationDeactivated && - controller.vacationPresentation.value.startDateIsNull, - hintText: AppLocalizations.of(context).startDate, - tapActionCallback: (value) => - controller.selectDate(context, DateType.start, value)), - const SizedBox(height: 18), - BorderButtonField( - label: AppLocalizations.of(context).startTime, - value: controller.vacationPresentation.value.startTime, - mouseCursor: SystemMouseCursors.text, - backgroundColor: AppColor.colorBackgroundVacationSettingField, - isEmpty: !controller.isVacationDeactivated && - controller.vacationPresentation.value.starTimeIsNull, - hintText: AppLocalizations.of(context).noStartTime, - tapActionCallback: (value) => - controller.selectTime(context, DateType.start, value)), - ]) - : Row(children: [ - Expanded(child: BorderButtonField( - label: AppLocalizations.of(context).startDate, - value: controller.vacationPresentation.value.startDate, - mouseCursor: SystemMouseCursors.text, - backgroundColor: AppColor.colorBackgroundVacationSettingField, - isEmpty: !controller.isVacationDeactivated && - controller.vacationPresentation.value.startDateIsNull, - hintText: AppLocalizations.of(context).startDate, - tapActionCallback: (value) => - controller.selectDate(context, DateType.start, value))), - const SizedBox(width: 24), - Expanded(child: BorderButtonField( - label: AppLocalizations.of(context).startTime, - value: controller.vacationPresentation.value.startTime, - mouseCursor: SystemMouseCursors.text, - backgroundColor: AppColor.colorBackgroundVacationSettingField, - isEmpty: !controller.isVacationDeactivated && - controller.vacationPresentation.value.starTimeIsNull, - hintText: AppLocalizations.of(context).noStartTime, - tapActionCallback: (value) => - controller.selectTime(context, DateType.start, value))), - ]), - ), - )), - const SizedBox(height: 24), - Obx(() => AbsorbPointer( - absorbing: controller.isVacationDeactivated, - child: Opacity( - opacity: controller.isVacationDeactivated ? 0.3 : 1.0, - child: Row(children: [ - Obx(() => InkWell( - onTap: () { - final value = !controller.vacationPresentation.value.vacationStopEnabled; - controller.updateVacationPresentation(vacationStopEnabled: value); - }, - child: SvgPicture.asset( - controller.vacationPresentation.value.vacationStopEnabled - ? _imagePaths.icSwitchOn - : _imagePaths.icSwitchOff, - fit: BoxFit.fill, - width: 24, - height: 24) - ) - ), - const SizedBox(width: 16), - Expanded( - child: Text(AppLocalizations.of(context).vacationStopsAt, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.normal, - color: Colors.black) - ), - ) - ]), - ) + titleQuickStyleBottomSheet: AppLocalizations.of(context).quickStyles, + titleBackgroundBottomSheet: AppLocalizations.of(context).background, + titleForegroundBottomSheet: AppLocalizations.of(context).foreground, + ), + child: _buildVacationFormView(context) + ); + } + } + + Widget _buildVacationFormView(BuildContext context) { + return SettingDetailViewBuilder( + responsiveUtils: _responsiveUtils, + padding: const EdgeInsets.symmetric(horizontal: 4), + onTapGestureDetector: () => controller.clearFocusEditor(context), + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + controller: controller.scrollController, + child: Padding( + padding: VacationUtils.getPaddingView(context, _responsiveUtils), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_responsiveUtils.isWebDesktop(context)) + ...[ + Text( + AppLocalizations.of(context).vacation, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + color: Colors.black + ) + ), + const SizedBox(height: 8) + ], + Text( + AppLocalizations.of(context).vacationSettingExplanation, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: AppColor.colorVacationSettingExplanation + ) + ), + const SizedBox(height: 24), + Row(children: [ + Obx(() { + return InkWell( + onTap: () { + final newStatus = controller.isVacationDeactivated + ? VacationResponderStatus.activated + : VacationResponderStatus.deactivated; + controller.updateVacationPresentation(newStatus: newStatus); + }, + child: SvgPicture.asset( + controller.isVacationDeactivated ? _imagePaths.icSwitchOff : _imagePaths.icSwitchOn, + fit: BoxFit.fill, + width: 24, + height: 24 + ) + ); + }), + const SizedBox(width: 16), + Expanded( + child: Text( + AppLocalizations.of(context).vacationSettingToggleButtonAutoReply, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black + ) + ), + ) + ]), + const SizedBox(height: 28), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + child: Column(children: [ + Obx(() => AbsorbPointer( + absorbing: controller.isVacationDeactivated, + child: Opacity( + opacity: controller.isVacationDeactivated ? 0.3 : 1.0, + child: _responsiveUtils.isPortraitMobile(context) + ? Column(children: [ + BorderButtonField( + label: AppLocalizations.of(context).startDate, + value: controller.vacationPresentation.value.startDate, + mouseCursor: SystemMouseCursors.text, + backgroundColor: AppColor.colorBackgroundVacationSettingField, + isEmpty: !controller.isVacationDeactivated && controller.vacationPresentation.value.startDateIsNull, + hintText: AppLocalizations.of(context).startDate, + tapActionCallback: (value) => controller.selectDate(context, DateType.start, value)), + const SizedBox(height: 18), + BorderButtonField( + label: AppLocalizations.of(context).startTime, + value: controller.vacationPresentation.value.startTime, + mouseCursor: SystemMouseCursors.text, + backgroundColor: AppColor.colorBackgroundVacationSettingField, + isEmpty: !controller.isVacationDeactivated && controller.vacationPresentation.value.starTimeIsNull, + hintText: AppLocalizations.of(context).noStartTime, + tapActionCallback: (value) => controller.selectTime(context, DateType.start, value)), + ]) + : Row(children: [ + Expanded(child: BorderButtonField( + label: AppLocalizations.of(context).startDate, + value: controller.vacationPresentation.value.startDate, + mouseCursor: SystemMouseCursors.text, + backgroundColor: AppColor.colorBackgroundVacationSettingField, + isEmpty: !controller.isVacationDeactivated && controller.vacationPresentation.value.startDateIsNull, + hintText: AppLocalizations.of(context).startDate, + tapActionCallback: (value) => controller.selectDate(context, DateType.start, value))), + const SizedBox(width: 24), + Expanded(child: BorderButtonField( + label: AppLocalizations.of(context).startTime, + value: controller.vacationPresentation.value.startTime, + mouseCursor: SystemMouseCursors.text, + backgroundColor: AppColor.colorBackgroundVacationSettingField, + isEmpty: !controller.isVacationDeactivated && controller.vacationPresentation.value.starTimeIsNull, + hintText: AppLocalizations.of(context).noStartTime, + tapActionCallback: (value) => controller.selectTime(context, DateType.start, value))), + ]), + ), + )), + const SizedBox(height: 24), + Obx(() => AbsorbPointer( + absorbing: controller.isVacationDeactivated, + child: Opacity( + opacity: controller.isVacationDeactivated ? 0.3 : 1.0, + child: Row(children: [ + Obx(() => InkWell( + onTap: () { + final value = !controller.vacationPresentation.value.vacationStopEnabled; + controller.updateVacationPresentation(vacationStopEnabled: value); + }, + child: SvgPicture.asset( + controller.vacationPresentation.value.vacationStopEnabled + ? _imagePaths.icSwitchOn + : _imagePaths.icSwitchOff, + fit: BoxFit.fill, + width: 24, + height: 24 + ) )), - const SizedBox(height: 24), - Obx(() => AbsorbPointer( - absorbing: !controller.canChangeEndDate, - child: Opacity( - opacity: !controller.canChangeEndDate ? 0.3 : 1.0, - child: _responsiveUtils.isPortraitMobile(context) - ? Column(children: [ - BorderButtonField( - label: AppLocalizations.of(context).endDate, - value: controller.vacationPresentation.value.endDate, - mouseCursor: SystemMouseCursors.text, - backgroundColor: AppColor.colorBackgroundVacationSettingField, - isEmpty: controller.canChangeEndDate && - controller.vacationPresentation.value.endDateIsNull, - hintText: AppLocalizations.of(context).noEndDate, - tapActionCallback: (value) => - controller.selectDate(context, DateType.end, value)), - const SizedBox(height: 18), - BorderButtonField( - label: AppLocalizations.of(context).endTime, - value: controller.vacationPresentation.value.endTime, - mouseCursor: SystemMouseCursors.text, - backgroundColor: AppColor.colorBackgroundVacationSettingField, - isEmpty: controller.canChangeEndDate && - controller.vacationPresentation.value.endTimeIsNull, - hintText: AppLocalizations.of(context).noEndTime, - tapActionCallback: (value) => - controller.selectTime(context, DateType.end, value)), - ]) - : Row(children: [ - Expanded(child: BorderButtonField( - label: AppLocalizations.of(context).endDate, - value: controller.vacationPresentation.value.endDate, - mouseCursor: SystemMouseCursors.text, - backgroundColor: AppColor.colorBackgroundVacationSettingField, - isEmpty: controller.canChangeEndDate && - controller.vacationPresentation.value.endDateIsNull, - hintText: AppLocalizations.of(context).noEndDate, - tapActionCallback: (value) => - controller.selectDate(context, DateType.end, value))), - const SizedBox(width: 24), - Expanded(child: BorderButtonField( - label: AppLocalizations.of(context).endTime, - value: controller.vacationPresentation.value.endTime, - mouseCursor: SystemMouseCursors.text, - backgroundColor: AppColor.colorBackgroundVacationSettingField, - isEmpty: controller.canChangeEndDate && - controller.vacationPresentation.value.endTimeIsNull, - hintText: AppLocalizations.of(context).noEndTime, - tapActionCallback: (value) => - controller.selectTime(context, DateType.end, value))), - ]), + const SizedBox(width: 16), + Expanded( + child: Text( + AppLocalizations.of(context).vacationStopsAt, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + color: Colors.black ) - )), - const SizedBox(height: 24), - Obx(() => AbsorbPointer( - absorbing: controller.isVacationDeactivated, - child: _responsiveUtils.isPortraitMobile(context) - ? Opacity( - opacity: controller.isVacationDeactivated ? 0.3 : 1.0, - child: TextInputFieldBuilder( - label: AppLocalizations.of(context).subject, - hint: AppLocalizations.of(context).hintSubjectInputVacationSetting, - editingController: controller.subjectTextController), - ) - : Row(children: [ - Expanded(child: Opacity( - opacity: controller.isVacationDeactivated ? 0.3 : 1.0, - child: TextInputFieldBuilder( - label: AppLocalizations.of(context).subject, - hint: AppLocalizations.of(context).hintSubjectInputVacationSetting, - editingController: controller.subjectTextController), - )), - const SizedBox(width: 24), - const Expanded(child: SizedBox.shrink()) - ]) - )), - const SizedBox(height: 24), - Obx(() => AbsorbPointer( - absorbing: controller.isVacationDeactivated, - child: Opacity( - opacity: controller.isVacationDeactivated ? 0.3 : 1.0, - child: _buildVacationMessage(context), ), - )), - const SizedBox(height: 24), - _buildListButtonAction(context) + ) ]), ) - ] - ), - ), + )), + const SizedBox(height: 24), + Obx(() => AbsorbPointer( + absorbing: !controller.canChangeEndDate, + child: Opacity( + opacity: !controller.canChangeEndDate ? 0.3 : 1.0, + child: _responsiveUtils.isPortraitMobile(context) + ? Column(children: [ + BorderButtonField( + label: AppLocalizations.of(context).endDate, + value: controller.vacationPresentation.value.endDate, + mouseCursor: SystemMouseCursors.text, + backgroundColor: AppColor.colorBackgroundVacationSettingField, + isEmpty: controller.canChangeEndDate && controller.vacationPresentation.value.endDateIsNull, + hintText: AppLocalizations.of(context).noEndDate, + tapActionCallback: (value) => controller.selectDate(context, DateType.end, value)), + const SizedBox(height: 18), + BorderButtonField( + label: AppLocalizations.of(context).endTime, + value: controller.vacationPresentation.value.endTime, + mouseCursor: SystemMouseCursors.text, + backgroundColor: AppColor.colorBackgroundVacationSettingField, + isEmpty: controller.canChangeEndDate && controller.vacationPresentation.value.endTimeIsNull, + hintText: AppLocalizations.of(context).noEndTime, + tapActionCallback: (value) => controller.selectTime(context, DateType.end, value)), + ]) + : Row(children: [ + Expanded(child: BorderButtonField( + label: AppLocalizations.of(context).endDate, + value: controller.vacationPresentation.value.endDate, + mouseCursor: SystemMouseCursors.text, + backgroundColor: AppColor.colorBackgroundVacationSettingField, + isEmpty: controller.canChangeEndDate && controller.vacationPresentation.value.endDateIsNull, + hintText: AppLocalizations.of(context).noEndDate, + tapActionCallback: (value) => controller.selectDate(context, DateType.end, value))), + const SizedBox(width: 24), + Expanded(child: BorderButtonField( + label: AppLocalizations.of(context).endTime, + value: controller.vacationPresentation.value.endTime, + mouseCursor: SystemMouseCursors.text, + backgroundColor: AppColor.colorBackgroundVacationSettingField, + isEmpty: controller.canChangeEndDate && controller.vacationPresentation.value.endTimeIsNull, + hintText: AppLocalizations.of(context).noEndTime, + tapActionCallback: (value) => controller.selectTime(context, DateType.end, value))), + ]), + ) + )), + const SizedBox(height: 24), + Obx(() => AbsorbPointer( + absorbing: controller.isVacationDeactivated, + child: _responsiveUtils.isPortraitMobile(context) + ? Opacity( + opacity: controller.isVacationDeactivated ? 0.3 : 1.0, + child: TextInputFieldBuilder( + label: AppLocalizations.of(context).subject, + hint: AppLocalizations.of(context).hintSubjectInputVacationSetting, + editingController: controller.subjectTextController, + focusNode: controller.subjectTextFocusNode, + ), + ) + : Row(children: [ + Expanded(child: Opacity( + opacity: controller.isVacationDeactivated ? 0.3 : 1.0, + child: TextInputFieldBuilder( + label: AppLocalizations.of(context).subject, + hint: AppLocalizations.of(context).hintSubjectInputVacationSetting, + editingController: controller.subjectTextController, + focusNode: controller.subjectTextFocusNode, + ), + )), + const SizedBox(width: 24), + const Expanded(child: SizedBox.shrink()) + ]) + )), + const SizedBox(height: 24), + Obx(() => AbsorbPointer( + absorbing: controller.isVacationDeactivated, + child: Opacity( + opacity: controller.isVacationDeactivated ? 0.3 : 1.0, + child: _buildVacationMessage(context), + ), + )), + const SizedBox(height: 24), + _buildListButtonAction(context) + ]), + ) + ] ), ), ), @@ -374,92 +367,66 @@ class VacationView extends GetWidget with RichTextButtonMixi } Widget _buildVacationMessage(BuildContext context) { - return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row(children: [ - Expanded(child: Text(AppLocalizations.of(context).message, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: AppColor.colorContentEmail))), - _buildVacationMessageTypeButton(context, VacationMessageType.plainText), - _buildVacationMessageTypeButton(context, VacationMessageType.htmlTemplate), - ]), - const SizedBox(height: 8), - _buildMessageTextEditor(context) - ]); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context).message, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: AppColor.colorContentEmail + ) + ), + const SizedBox(height: 8), + _buildMessageTextEditor(context) + ] + ); } Widget _buildMessageTextEditor(BuildContext context) { - return Obx(() { - if (controller.vacationMessageType.value == VacationMessageType.plainText) { - return _buildMessagePlainTextEditor(context); - } else { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColor.colorInputBorderCreateMailbox), - color: Colors.white), - padding: const EdgeInsets.only(left: 12, right: 12, top: 12), - child: Column(children: [ - _buildMessageHtmlTextEditor(context), - if (BuildUtils.isWeb) - Center(child: Obx(() { - return PointerInterceptor( - child: buildToolbarRichTextForWeb( - context, - controller.richTextControllerForWeb, - layoutType: ButtonLayoutType.scrollHorizontal), - ); - })) - ]), - ); - } - }); - } - - Widget _buildMessagePlainTextEditor(BuildContext context) { - return (TextFieldBuilder() - ..onChange((value) => controller.updateMessageBody(context, value)) - ..textInputAction(TextInputAction.next) - ..addController(controller.messageTextController) - ..textStyle(const TextStyle(color: Colors.black, fontSize: 16)) - ..keyboardType(TextInputType.text) - ..minLines(10) - ..maxLines(null) - ..textDecoration((TextInputDecorationBuilder() - ..setContentPadding(const EdgeInsets.all(16)) - ..setHintText(AppLocalizations.of(context).hintMessageBodyVacation) - ..setFillColor(Colors.white) - ..setErrorText(controller.isVacationDeactivated - ? null - : controller.errorMessageBody.value)) - .build())) - .build(); + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColor.colorInputBorderCreateMailbox), + color: Colors.white), + padding: const EdgeInsetsDirectional.only(start: 12, end: 12, top: 12), + child: Column(children: [ + _buildMessageHtmlTextEditor(context), + if (PlatformInfo.isWeb) + Center( + child: PointerInterceptor( + child: buildToolbarRichTextForWeb( + context, + controller.richTextControllerForWeb, + layoutType: ButtonLayoutType.scrollHorizontal + ) + ) + ) + ]), + ); } Widget _buildMessageHtmlTextEditor(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return html_editor_browser.HtmlEditor( key: const Key('vacation_message_html_text_editor_web'), controller: controller.richTextControllerForWeb.editorController, - htmlEditorOptions: const html_editor_browser.HtmlEditorOptions( - hint: '', - darkMode: false, - customBodyCssStyle: bodyCssStyleForEditor), - blockQuotedContent: controller.vacationMessageHtmlText ?? '', + htmlEditorOptions: html_editor_browser.HtmlEditorOptions( + hint: '', + darkMode: false, + initialText: controller.vacationMessageHtmlText, + customBodyCssStyle: HtmlUtils.customCssStyleHtmlEditor(direction: AppUtils.getCurrentDirection(context)) + ), htmlToolbarOptions: const html_editor_browser.HtmlToolbarOptions( toolbarType: html_editor_browser.ToolbarType.hide, defaultToolbarButtons: []), otherOptions: const html_editor_browser.OtherOptions(height: 150), callbacks: html_editor_browser.Callbacks( - onInit: () { - controller.richTextControllerForWeb.setFullScreenEditor(); - }, onChangeSelection: (settings) { - controller.richTextControllerForWeb.onEditorSettingsChange(settings); - }, onChangeContent: (String? changed) { - controller.updateMessageHtmlText(changed); - }, onFocus: () { - FocusScope.of(context).unfocus(); + onChangeSelection: controller.richTextControllerForWeb.onEditorSettingsChange, + onChangeContent: controller.updateMessageHtmlText, + onFocus: () { + KeyboardUtils.hideKeyboard(context); Future.delayed(const Duration(milliseconds: 500), () { controller.richTextControllerForWeb.editorController.setFocus(); }); @@ -468,11 +435,12 @@ class VacationView extends GetWidget with RichTextButtonMixi ), ); } else { - return html_editor_mobile.HtmlEditor( + return HtmlEditor( key: controller.htmlKey, minHeight: controller.htmlEditorMinHeight, addDefaultSelectionMenuItems: false, initialContent: controller.vacationMessageHtmlText ?? '', + customStyleCss: HtmlUtils.customCssStyleHtmlEditor(direction: AppUtils.getCurrentDirection(context)), onCreated: (htmlApi) { controller.richTextControllerForMobile.onCreateHTMLEditor( htmlApi, @@ -484,21 +452,4 @@ class VacationView extends GetWidget with RichTextButtonMixi ); } } - - Widget _buildVacationMessageTypeButton(BuildContext context, VacationMessageType messageType) { - return buildButtonWrapText( - messageType.getTitle(context), - textStyle: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14, - color: controller.vacationMessageType.value == messageType - ? AppColor.colorContentEmail - : AppColor.colorHintSearchBar), - bgColor: controller.vacationMessageType.value == messageType - ? AppColor.emailAddressChipColor - : Colors.transparent, - height: 35, - radius: 10, - onTap: () => controller.selectVacationMessageType(context, messageType)); - } } \ No newline at end of file diff --git a/lib/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart b/lib/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart index 03d6be0a73..4dfb120c3d 100644 --- a/lib/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart +++ b/lib/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart @@ -106,7 +106,7 @@ class VacationNotificationMessageWidget extends StatelessWidget { if (leadingIcon != null) leadingIcon!, Expanded( child: Padding( - padding: const EdgeInsets.only(right: 12), + padding: const EdgeInsets.only(right: 12, top: 12), child: Center( child: Text( vacationResponse.getNotificationMessage(context), diff --git a/lib/features/network_connection/presentation/network_connection_banner_widget.dart b/lib/features/network_connection/presentation/network_connection_banner_widget.dart new file mode 100644 index 0000000000..5ab64437f7 --- /dev/null +++ b/lib/features/network_connection/presentation/network_connection_banner_widget.dart @@ -0,0 +1,34 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class NetworkConnectionBannerWidget extends StatelessWidget { + + const NetworkConnectionBannerWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + color: AppColor.colorNetworkConnectionBannerBackground, + width: double.infinity, + padding: const EdgeInsetsDirectional.symmetric(vertical: 6, horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CupertinoActivityIndicator(), + const SizedBox(width: 8), + Text( + AppLocalizations.of(context).no_internet_connection, + textAlign: TextAlign.center, + style: const TextStyle( + color: AppColor.colorNetworkConnectionLabel, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/network_connection/presentation/network_connection_controller.dart b/lib/features/network_connection/presentation/network_connection_controller.dart new file mode 100644 index 0000000000..897ae1b3ac --- /dev/null +++ b/lib/features/network_connection/presentation/network_connection_controller.dart @@ -0,0 +1,99 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:get/get.dart'; +import 'package:internet_connection_checker/internet_connection_checker.dart'; + +class NetworkConnectionController extends GetxController { + static const Duration _timeoutInternetConnection = Duration(milliseconds: 5000); + static const Duration _timeIntervalInternetConnection = Duration(milliseconds: 5000); + + final _connectivityResult = Rxn(); + final _internetConnectionStatus = Rxn(); + + final Connectivity _connectivity; + + final _internetConnectionChecker = InternetConnectionChecker.createInstance( + checkTimeout: _timeoutInternetConnection, + checkInterval: _timeIntervalInternetConnection + ); + + StreamSubscription? _subscription; + StreamSubscription? _internetSubscription; + + NetworkConnectionController(this._connectivity); + + @override + void onInit() { + super.onInit(); + _listenNetworkConnectionChanged(); + } + + @override + void onReady() { + super.onReady(); + _getCurrentNetworkConnectionState(); + } + + @override + void onClose() { + _subscription?.cancel(); + if (PlatformInfo.isMobile) { + _internetSubscription?.cancel(); + } + super.onClose(); + } + + void _getCurrentNetworkConnectionState() async { + final listConnectionResult = await Future.wait([ + _connectivity.checkConnectivity(), + _internetConnectionChecker.connectionStatus, + ]); + log('NetworkConnectionController::_getCurrentNetworkConnectionState():listConnectionResult: $listConnectionResult'); + + if (listConnectionResult[0] is ConnectivityResult) { + _setNetworkConnectivityState(listConnectionResult[0] as ConnectivityResult); + } + + if (listConnectionResult[1] is InternetConnectionStatus) { + _setInternetConnectivityStatus(listConnectionResult[1] as InternetConnectionStatus); + } + } + + void _listenNetworkConnectionChanged() { + _subscription = _connectivity.onConnectivityChanged.listen( + (result) { + log('NetworkConnectionController::_listenNetworkConnectionChanged()::onConnectivityChanged: $result'); + _setNetworkConnectivityState(result); + }, + onError: (error, stackTrace) { + logError('NetworkConnectionController::_listenNetworkConnectionChanged()::onConnectivityChanged:error: $error | stackTrace: $stackTrace'); + } + ); + + _internetSubscription = _internetConnectionChecker.onStatusChange.listen( + (status) { + log('NetworkConnectionController::_listenNetworkConnectionChanged()::onStatusChange: $status'); + _setInternetConnectivityStatus(status); + }, + onError: (error, stackTrace) { + logError('NetworkConnectionController::_listenNetworkConnectionChanged()::onStatusChange:error: $error | stackTrace: $stackTrace'); + } + ); + } + + void _setNetworkConnectivityState(ConnectivityResult newConnectivityResult) { + _connectivityResult.value = newConnectivityResult; + } + + void _setInternetConnectivityStatus(InternetConnectionStatus newStatus) { + _internetConnectionStatus.value = newStatus; + } + + bool isNetworkConnectionAvailable() { + return _connectivityResult.value != ConnectivityResult.none && + _internetConnectionStatus.value == InternetConnectionStatus.connected; + } +} \ No newline at end of file diff --git a/lib/features/network_connection/presentation/web_network_connection_controller.dart b/lib/features/network_connection/presentation/web_network_connection_controller.dart new file mode 100644 index 0000000000..4afdb5be73 --- /dev/null +++ b/lib/features/network_connection/presentation/web_network_connection_controller.dart @@ -0,0 +1,97 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/presentation/views/toast/tmail_toast.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class NetworkConnectionController extends GetxController { + final _imagePaths = Get.find(); + final _appToast = Get.find(); + + final _connectivityResult = Rxn(); + + final Connectivity _connectivity; + + bool _isEnableShowToastDisconnection = true; + + StreamSubscription? _subscription; + + NetworkConnectionController(this._connectivity); + + @override + void onInit() { + super.onInit(); + _listenNetworkConnectionChanged(); + } + + @override + void onReady() { + super.onReady(); + _getCurrentNetworkConnectionState(); + } + + @override + void onClose() { + _subscription?.cancel(); + super.onClose(); + } + + void _getCurrentNetworkConnectionState() async { + final connectionResult = await _connectivity.checkConnectivity(); + log('NetworkConnectionController::_getCurrentNetworkConnectionState():connectionResult: $connectionResult'); + _setNetworkConnectivityState(connectionResult); + _handleNetworkConnectionState(); + } + + void _listenNetworkConnectionChanged() { + _subscription = _connectivity.onConnectivityChanged.listen( + (result) { + log('NetworkConnectionController::_listenNetworkConnectionChanged()::onConnectivityChanged: $result'); + _setNetworkConnectivityState(result); + _handleNetworkConnectionState(); + }, + onError: (error, stackTrace) { + logError('NetworkConnectionController::_listenNetworkConnectionChanged()::onConnectivityChanged:error: $error | stackTrace: $stackTrace'); + } + ); + } + + void _setNetworkConnectivityState(ConnectivityResult newConnectivityResult) { + _connectivityResult.value = newConnectivityResult; + } + + bool isNetworkConnectionAvailable() => _connectivityResult.value != ConnectivityResult.none; + + void _handleNetworkConnectionState() { + if (_isEnableShowToastDisconnection && !isNetworkConnectionAvailable()) { + _showToastLostConnection(); + } else { + ToastView.dismiss(); + } + } + + void _showToastLostConnection() { + if (currentContext != null && currentOverlayContext != null) { + _appToast.showToastMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).no_internet_connection, + actionName: AppLocalizations.of(currentContext!).skip, + onActionClick: () { + _isEnableShowToastDisconnection = false; + ToastView.dismiss(); + }, + leadingSVGIcon: _imagePaths.icNotConnection, + backgroundColor: AppColor.textFieldErrorBorderColor, + textColor: Colors.white, + infinityToast: true, + ); + } + } +} \ No newline at end of file diff --git a/lib/features/network_status_handle/presentation/network_connnection_controller.dart b/lib/features/network_status_handle/presentation/network_connnection_controller.dart deleted file mode 100644 index ebcbc3473e..0000000000 --- a/lib/features/network_status_handle/presentation/network_connnection_controller.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'dart:async'; - -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/base/base_controller.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; -import 'package:tmail_ui_user/main/routes/route_navigation.dart'; - -class NetworkConnectionController extends BaseController { - final connectivityResult = Rxn(); - final _imagePaths = Get.find(); - final Connectivity _connectivity; - final AppToast _appToast = Get.find(); - final ResponsiveUtils _responsiveUtils = Get.find(); - - bool _isEnableShowToastDisconnection = true; - - late StreamSubscription subscription; - - NetworkConnectionController(this._connectivity); - - @override - void onInit() { - super.onInit(); - log('NetworkConnectionController::onInit():'); - _listenNetworkConnectionChanged(); - } - - @override - void onReady() { - super.onReady(); - log('NetworkConnectionController::onReady():'); - _getCurrentNetworkConnectionState(); - } - - @override - void onClose() { - subscription.cancel(); - super.onClose(); - } - - @override - void onDone() {} - - void _getCurrentNetworkConnectionState() async { - final currentConnectionResult = await _connectivity.checkConnectivity(); - log('NetworkConnectionController::onReady():_getCurrentNetworkConnectionState: $currentConnectionResult'); - _setNetworkConnectivityState(currentConnectionResult); - if (_isEnableShowToastDisconnection && !isNetworkConnectionAvailable()) { - _showToastLostConnection(); - } else { - ToastView.dismiss(); - } - } - - void _listenNetworkConnectionChanged() { - subscription = _connectivity.onConnectivityChanged.listen( - (result) { - log('NetworkConnectionController::_listenNetworkConnectionChanged():onConnectivityChanged: $result'); - _setNetworkConnectivityState(result); - if (_isEnableShowToastDisconnection && !isNetworkConnectionAvailable()) { - _showToastLostConnection(); - } else { - ToastView.dismiss(); - } - }, - onError: (error, stackTrace) { - logError('NetworkConnectionController::_listenNetworkConnectionChanged():error: $error'); - logError('NetworkConnectionController::_listenNetworkConnectionChanged():stackTrace: $stackTrace'); - } - ); - } - - void _setNetworkConnectivityState(ConnectivityResult newConnectivityResult) { - connectivityResult.value = newConnectivityResult; - } - - bool isNetworkConnectionAvailable() { - return connectivityResult.value != ConnectivityResult.none; - } - - void _showToastLostConnection() { - if (currentContext != null && currentOverlayContext != null) { - _appToast.showBottomToast( - currentOverlayContext!, - AppLocalizations.of(currentContext!).no_internet_connection, - actionName: AppLocalizations.of(currentContext!).skip, - onActionClick: () { - _isEnableShowToastDisconnection = false; - ToastView.dismiss(); - }, - leadingIcon: SvgPicture.asset( - _imagePaths.icNotConnection, - width: 24, - height: 24, - fit: BoxFit.fill), - backgroundColor: AppColor.textFieldErrorBorderColor, - textColor: Colors.white, - textActionColor: Colors.white, - maxWidth: _responsiveUtils.getMaxWidthToast(currentContext!), - infinityToast: true, - ); - } - } -} \ No newline at end of file diff --git a/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart b/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart new file mode 100644 index 0000000000..cd5f6d8304 --- /dev/null +++ b/lib/features/offline_mode/bindings/sending_email_interactor_bindings.dart @@ -0,0 +1,104 @@ +import 'package:core/data/model/source_type/data_source_type.dart'; +import 'package:core/utils/file_utils.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; +import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; +import 'package:tmail_ui_user/features/email/data/repository/email_repository_impl.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; +import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; +import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart'; +import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart'; +import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/state_datasource_impl.dart'; +import 'package:tmail_ui_user/features/mailbox/data/local/mailbox_cache_manager.dart'; +import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_api.dart'; +import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_isolate_worker.dart'; +import 'package:tmail_ui_user/features/mailbox/data/repository/mailbox_repository_impl.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart'; +import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; + +class SendEmailInteractorBindings extends InteractorsBindings { + + @override + void bindingsDataSource() { + Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsDataSourceImpl() { + Get.lazyPut(() => EmailDataSourceImpl( + Get.find(), + Get.find())); + Get.lazyPut(() => MailboxDataSourceImpl( + Get.find(), + Get.find(), + Get.find())); + Get.lazyPut(() => MailboxCacheDataSourceImpl( + Get.find(), + Get.find())); + Get.lazyPut(() => HtmlDataSourceImpl( + Get.find(), + Get.find())); + Get.lazyPut(() => StateDataSourceImpl( + Get.find(), + Get.find())); + Get.lazyPut(() => EmailHiveCacheDataSourceImpl( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find())); + } + + @override + void bindingsInteractor() { + Get.lazyPut(() => SendEmailInteractor(Get.find(), Get.find())); + } + + @override + void bindingsRepository() { + Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsRepositoryImpl() { + Get.lazyPut(() => EmailRepositoryImpl( + { + DataSourceType.network: Get.find(), + DataSourceType.hiveCache: Get.find() + }, + Get.find(), + Get.find(), + )); + Get.lazyPut(() => MailboxRepositoryImpl( + { + DataSourceType.network: Get.find(), + DataSourceType.local: Get.find() + }, + Get.find(), + )); + } + +} \ No newline at end of file diff --git a/lib/features/offline_mode/config/work_manager_config.dart b/lib/features/offline_mode/config/work_manager_config.dart new file mode 100644 index 0000000000..77ec26f9d3 --- /dev/null +++ b/lib/features/offline_mode/config/work_manager_config.dart @@ -0,0 +1,19 @@ + +import 'package:core/core.dart'; +import 'package:tmail_ui_user/features/offline_mode/work_manager/work_dispatcher.dart'; +import 'package:workmanager/workmanager.dart'; + +class WorkManagerConfig { + static WorkManagerConfig? _instance; + + WorkManagerConfig._(); + + factory WorkManagerConfig() => _instance ??= WorkManagerConfig._(); + + Future initialize() { + return Workmanager().initialize( + callbackDispatcher, + isInDebugMode: BuildUtils.isDebugMode + ); + } +} \ No newline at end of file diff --git a/lib/features/offline_mode/config/work_manager_constants.dart b/lib/features/offline_mode/config/work_manager_constants.dart new file mode 100644 index 0000000000..6da0fc1264 --- /dev/null +++ b/lib/features/offline_mode/config/work_manager_constants.dart @@ -0,0 +1,6 @@ + +class WorkManagerConstants { + static const String sendingEmailUniqueId = 'com.linagora.ios.teammail.sendingQueue'; + static const String workerTypeKey = 'worker_type'; + static const int delayTime = 2000; +} \ No newline at end of file diff --git a/lib/features/offline_mode/controller/work_manager_controller.dart b/lib/features/offline_mode/controller/work_manager_controller.dart new file mode 100644 index 0000000000..1b8865693f --- /dev/null +++ b/lib/features/offline_mode/controller/work_manager_controller.dart @@ -0,0 +1,92 @@ +import 'dart:async'; +import 'package:collection/collection.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:tmail_ui_user/features/offline_mode/config/work_manager_constants.dart'; +import 'package:tmail_ui_user/features/offline_mode/exceptions/workmanager_exception.dart'; +import 'package:tmail_ui_user/features/offline_mode/work_manager/one_time_work_request.dart'; +import 'package:tmail_ui_user/features/offline_mode/work_manager/periodic_work_request.dart'; +import 'package:tmail_ui_user/features/offline_mode/work_manager/work_request.dart'; +import 'package:tmail_ui_user/features/offline_mode/work_manager/worker_type.dart'; +import 'package:workmanager/workmanager.dart'; + +class WorkManagerController { + + static WorkManagerController? _instance; + + WorkManagerController._(); + + factory WorkManagerController() => _instance ??= WorkManagerController._(); + + Future enqueue(WorkRequest workRequest) async { + try { + log('WorkSchedulerController::enqueue():workRequest: $workRequest'); + if (workRequest is OneTimeWorkRequest) { + await Workmanager().registerOneOffTask( + workRequest.uniqueId, + workRequest.taskId, + tag: workRequest.tag, + initialDelay: workRequest.initialDelay, + constraints: workRequest.constraints, + backoffPolicy: workRequest.backoffPolicy, + backoffPolicyDelay: workRequest.backoffPolicyDelay, + outOfQuotaPolicy: workRequest.outOfQuotaPolicy, + inputData: workRequest.inputData + ); + } if (workRequest is PeriodicWorkRequest) { + await Workmanager().registerPeriodicTask( + workRequest.uniqueId, + workRequest.taskId, + tag: workRequest.tag, + frequency: workRequest.frequency, + initialDelay: workRequest.initialDelay, + constraints: workRequest.constraints, + backoffPolicy: workRequest.backoffPolicy, + backoffPolicyDelay: workRequest.backoffPolicyDelay, + outOfQuotaPolicy: workRequest.outOfQuotaPolicy, + inputData: workRequest.inputData + ); + } + } catch (e) { + logError('WorkSchedulerController::enqueue(): EXCEPTION: $e'); + } + } + + Future handleBackgroundTask(String taskName, Map? inputData) async { + log('WorkSchedulerController::handleBackgroundTask():taskName: $taskName | inputData: $inputData'); + try { + if (inputData != null && inputData.isNotEmpty) { + final workerType = inputData.remove(WorkManagerConstants.workerTypeKey); + final dataObject = inputData; + log('WorkSchedulerController::handleBackgroundTask():workerType: $workerType | dataObject: $dataObject'); + final matchedType = WorkerType.values.firstWhereOrNull((type) => type.name == workerType); + + if (matchedType != null) { + final worker = matchedType.getWorker(); + await worker.bindDI(); + final result = await worker.doWork(taskName, dataObject); + return result; + } else { + return Future.error(CanNotFoundWorkerType()); + } + } else { + return Future.error(CanNotFoundInputData()); + } + } catch (e) { + logError('WorkSchedulerController::handleBackgroundTask():EXCEPTION: $e'); + return Future.error(e); + } + } + + Future cancelByWorkType(WorkerType type) => Workmanager().cancelByTag(type.name); + + Future cancelByUniqueId(String uniqueId) { + try { + return Workmanager().cancelByUniqueName(uniqueId); + } catch (e) { + logError('WorkSchedulerController::cancelByUniqueId():EXCEPTION: $e'); + return Future.value(); + } + } + + Future cancelAll() => Workmanager().cancelAll(); +} \ No newline at end of file diff --git a/lib/features/offline_mode/exceptions/workmanager_exception.dart b/lib/features/offline_mode/exceptions/workmanager_exception.dart new file mode 100644 index 0000000000..a1204f0915 --- /dev/null +++ b/lib/features/offline_mode/exceptions/workmanager_exception.dart @@ -0,0 +1,3 @@ +class CanNotFoundInputData implements Exception {} + +class CanNotFoundWorkerType implements Exception {} \ No newline at end of file diff --git a/lib/features/offline_mode/extensions/list_detailed_email_hive_cache_extension.dart b/lib/features/offline_mode/extensions/list_detailed_email_hive_cache_extension.dart new file mode 100644 index 0000000000..d2b823a7be --- /dev/null +++ b/lib/features/offline_mode/extensions/list_detailed_email_hive_cache_extension.dart @@ -0,0 +1,12 @@ + +import 'package:jmap_dart_client/jmap/core/extensions/date_time_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/detailed_email_hive_cache.dart'; + +extension ListDetailedEmailHiveCacheExtension on List { + + void sortByLatestTime() { + sort((detailedEmail1, detailedEmail2) { + return detailedEmail1.timeSaved.compareToSort(detailedEmail2.timeSaved, false); + }); + } +} \ No newline at end of file diff --git a/lib/features/offline_mode/extensions/list_sending_email_hive_cache_extension.dart b/lib/features/offline_mode/extensions/list_sending_email_hive_cache_extension.dart new file mode 100644 index 0000000000..2c77644dc1 --- /dev/null +++ b/lib/features/offline_mode/extensions/list_sending_email_hive_cache_extension.dart @@ -0,0 +1,17 @@ + +import 'package:jmap_dart_client/jmap/core/extensions/date_time_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/extensions/sending_email_hive_cache_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_email_hive_cache.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +extension ListSendingEmailHiveCacheExtension on List { + + List toSendingEmails() => + map((sendingEmailCache) => sendingEmailCache.toSendingEmail()).toList(); + + void sortByLatestTime() { + sort((detailedEmail1, detailedEmail2) { + return detailedEmail1.createTime.compareToSort(detailedEmail2.createTime, false); + }); + } +} \ No newline at end of file diff --git a/lib/features/offline_mode/extensions/sending_email_hive_cache_extension.dart b/lib/features/offline_mode/extensions/sending_email_hive_cache_extension.dart new file mode 100644 index 0000000000..bcd44d55b2 --- /dev/null +++ b/lib/features/offline_mode/extensions/sending_email_hive_cache_extension.dart @@ -0,0 +1,32 @@ + +import 'dart:convert'; + +import 'package:jmap_dart_client/http/converter/email_id_nullable_converter.dart'; +import 'package:jmap_dart_client/http/converter/id_nullable_converter.dart'; +import 'package:jmap_dart_client/http/converter/identities/identity_id_nullable_converter.dart'; +import 'package:jmap_dart_client/http/converter/mailbox_id_nullable_converter.dart'; +import 'package:jmap_dart_client/http/converter/mailbox_name_converter.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_email_hive_cache.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +extension SendingEmailHiveCacheExtension on SendingEmailHiveCache { + + SendingEmail toSendingEmail() { + return SendingEmail( + sendingId: sendingId, + email: Email.fromJson(jsonDecode(email)), + emailActionType: EmailActionType.values.firstWhere((value) => value.name == emailActionType), + createTime: createTime, + sentMailboxId: const MailboxIdNullableConverter().fromJson(sentMailboxId), + emailIdDestroyed: const EmailIdNullableConverter().fromJson(emailIdDestroyed), + emailIdAnsweredOrForwarded: const EmailIdNullableConverter().fromJson(emailIdAnsweredOrForwarded), + identityId: const IdentityIdNullableConverter().fromJson(identityId), + mailboxNameRequest: const MailboxNameConverter().fromJson(mailboxNameRequest), + creationIdRequest: const IdNullableConverter().fromJson(creationIdRequest), + sendingState: SendingState.values.firstWhere((value) => value.name == sendingState) + ); + } +} \ No newline at end of file diff --git a/lib/features/offline_mode/hive_worker/hive_task.dart b/lib/features/offline_mode/hive_worker/hive_task.dart new file mode 100644 index 0000000000..347087b3e3 --- /dev/null +++ b/lib/features/offline_mode/hive_worker/hive_task.dart @@ -0,0 +1,44 @@ +import 'dart:async'; + +import 'package:equatable/equatable.dart'; +import 'package:tmail_ui_user/features/offline_mode/hive_worker/hive_task_state.dart'; + +class HiveTask with EquatableMixin { + final String? id; + final A? action; + final Future Function()? conditionInvoked; + final Future Function() runnable; + + HiveTask({ + required this.runnable, + this.id, + this.action, + this.conditionInvoked, + }); + + Future execute() async { + final resultCompleter = Completer(); + + try { + if (conditionInvoked != null) { + final invoked = await conditionInvoked!.call(); + if (invoked) { + final result = await runnable.call(); + resultCompleter.complete(TaskSuccess(result: result)); + } else { + resultCompleter.completeError(TaskFailure()); + } + } else { + final result = await runnable.call(); + resultCompleter.complete(TaskSuccess(result: result)); + } + } catch (e) { + resultCompleter.completeError(TaskFailure(exception: e)); + } + + return resultCompleter.future; + } + + @override + List get props => [runnable, id, action, conditionInvoked]; +} \ No newline at end of file diff --git a/lib/features/offline_mode/hive_worker/hive_task_state.dart b/lib/features/offline_mode/hive_worker/hive_task_state.dart new file mode 100644 index 0000000000..02ac85340a --- /dev/null +++ b/lib/features/offline_mode/hive_worker/hive_task_state.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class TaskSuccess extends Success { + final dynamic result; + + TaskSuccess({this.result}); + + @override + List get props => [result]; +} + +class TaskFailure extends Failure { + final dynamic exception; + + TaskFailure({this.exception}); + + @override + List get props => [exception]; +} \ No newline at end of file diff --git a/lib/features/offline_mode/hive_worker/hive_worker_queue.dart b/lib/features/offline_mode/hive_worker/hive_worker_queue.dart new file mode 100644 index 0000000000..f1188ce4d3 --- /dev/null +++ b/lib/features/offline_mode/hive_worker/hive_worker_queue.dart @@ -0,0 +1,65 @@ + +import 'dart:async'; +import 'dart:collection'; + +import 'package:core/utils/app_logger.dart'; +import 'package:tmail_ui_user/features/offline_mode/hive_worker/hive_task.dart'; + +abstract class WorkerQueue { + + final Queue> queue = Queue>(); + Completer? completer; + + String get workerName; + + Future addTask(HiveTask task) { + queue.add(task); + log('WorkerQueue<$workerName>::addTask(): QUEUE_LENGTH: ${queue.length}'); + return _processTask(); + } + + Future _processTask() async { + if (completer != null) { + return completer!.future; + } + completer = Completer(); + if (queue.isNotEmpty) { + final firstTask = queue.removeFirst(); + log('WorkerQueue<$workerName>::_processTask(): ${firstTask.id}'); + firstTask.execute() + .then(_handleTaskExecuteCompleted) + .catchError(_handleTaskExecuteError); + } else { + completer?.complete(); + } + return completer!.future; + } + + void _handleTaskExecuteCompleted(dynamic value) { + log('WorkerQueue<$workerName>::_handleTaskExecuteCompleted(): $value'); + completer?.complete(); + _releaseCompleter(); + if (queue.isNotEmpty) { + _processTask(); + } + } + + void _handleTaskExecuteError(error) { + log('WorkerQueue<$workerName>::_handleTaskExecuteError(): $error'); + completer?.complete(); + _releaseCompleter(); + if (queue.isNotEmpty) { + _processTask(); + } + } + + void _releaseCompleter() { + completer = null; + } + + Future release() async { + log('WorkerQueue<$workerName>::release():'); + queue.clear(); + _releaseCompleter(); + } +} \ No newline at end of file diff --git a/lib/features/offline_mode/manager/new_email_cache_manager.dart b/lib/features/offline_mode/manager/new_email_cache_manager.dart new file mode 100644 index 0000000000..83bf38b535 --- /dev/null +++ b/lib/features/offline_mode/manager/new_email_cache_manager.dart @@ -0,0 +1,87 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/file_utils.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/caching/clients/new_email_hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_cache_exceptions.dart'; +import 'package:tmail_ui_user/features/offline_mode/extensions/list_detailed_email_hive_cache_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/detailed_email_hive_cache.dart'; + +class NewEmailCacheManager { + + final NewEmailHiveCacheClient _cacheClient; + final FileUtils _fileUtils; + + NewEmailCacheManager(this._cacheClient, this._fileUtils); + + Future storeDetailedNewEmail( + AccountId accountId, + UserName userName, + DetailedEmailHiveCache detailedEmailCache + ) async { + final listDetailedEmails = await getAllDetailedEmails(accountId, userName); + log('NewEmailCacheManager::storeDetailedNewEmail():listDetailedEmails: $listDetailedEmails'); + if (listDetailedEmails.length >= CachingConstants.maxNumberNewEmailsForOffline) { + final lastElementsListEmail = listDetailedEmails.sublist(CachingConstants.maxNumberNewEmailsForOffline - 1); + for (var email in lastElementsListEmail) { + if (email.emailContentPath != null) { + await _deleteFileExisted(email.emailContentPath!); + } + await removeDetailedEmail(accountId, userName, email.emailId); + } + log('NewEmailCacheManager::storeDetailedNewEmail(): DELETE COMPLETED'); + } + await insertDetailedEmail(accountId, userName, detailedEmailCache); + log('NewEmailCacheManager::storeDetailedNewEmail(): INSERT COMPLETED'); + return detailedEmailCache; + } + + Future insertDetailedEmail( + AccountId accountId, + UserName userName, + DetailedEmailHiveCache detailedEmailCache + ) { + final keyCache = TupleKey(detailedEmailCache.emailId, accountId.asString, userName.value).encodeKey; + return _cacheClient.insertItem(keyCache, detailedEmailCache); + } + + Future removeDetailedEmail( + AccountId accountId, + UserName userName, + String emailId + ) { + final keyCache = TupleKey(emailId, accountId.asString, userName.value).encodeKey; + return _cacheClient.deleteItem(keyCache); + } + + Future> getAllDetailedEmails(AccountId accountId, UserName userName) async { + final detailedEmailCacheList = await _cacheClient.getListByTupleKey(accountId.asString, userName.value); + detailedEmailCacheList.sortByLatestTime(); + return detailedEmailCacheList; + } + + Future _deleteFileExisted(String pathFile) async { + await _fileUtils.deleteFile(pathFile); + } + + Future getStoredNewEmail( + AccountId accountId, + UserName userName, + EmailId emailId + ) async { + final keyCache = TupleKey(emailId.asString, accountId.asString, userName.value).encodeKey; + final detailedEmailCache = await _cacheClient.getItem(keyCache, needToReopen: true); + if (detailedEmailCache != null) { + return detailedEmailCache; + } else { + throw NotFoundStoredNewEmailException(); + } + } + + Future closeNewEmailHiveCacheBox() => _cacheClient.closeBox(); +} \ No newline at end of file diff --git a/lib/features/offline_mode/manager/new_email_cache_worker_queue.dart b/lib/features/offline_mode/manager/new_email_cache_worker_queue.dart new file mode 100644 index 0000000000..50f346f87f --- /dev/null +++ b/lib/features/offline_mode/manager/new_email_cache_worker_queue.dart @@ -0,0 +1,8 @@ + +import 'package:tmail_ui_user/features/offline_mode/hive_worker/hive_worker_queue.dart'; + +class NewEmailCacheWorkerQueue extends WorkerQueue { + + @override + String get workerName => 'NewEmailCache'; +} \ No newline at end of file diff --git a/lib/features/offline_mode/manager/opened_email_cache_manager.dart b/lib/features/offline_mode/manager/opened_email_cache_manager.dart new file mode 100644 index 0000000000..b6f791ab11 --- /dev/null +++ b/lib/features/offline_mode/manager/opened_email_cache_manager.dart @@ -0,0 +1,87 @@ +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/file_utils.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:model/extensions/email_id_extensions.dart'; +import 'package:tmail_ui_user/features/caching/clients/opened_email_hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_cache_exceptions.dart'; +import 'package:tmail_ui_user/features/offline_mode/extensions/list_detailed_email_hive_cache_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/detailed_email_hive_cache.dart'; + +class OpenedEmailCacheManager { + + final OpenedEmailHiveCacheClient _cacheClient; + final FileUtils _fileUtils; + + OpenedEmailCacheManager(this._cacheClient, this._fileUtils); + + Future insertDetailedEmail( + AccountId accountId, + UserName userName, + DetailedEmailHiveCache detailedEmailCache + ) { + final keyCache = TupleKey(detailedEmailCache.emailId, accountId.asString, userName.value).encodeKey; + log('OpenedEmailCacheManager::insertDetailedEmail(): $keyCache'); + return _cacheClient.insertItem(keyCache, detailedEmailCache); + } + + Future removeDetailedEmail( + AccountId accountId, + UserName userName, + String emailId + ) { + final keyCache = TupleKey(emailId, accountId.asString, userName.value).encodeKey; + log('OpenedEmailCacheManager::removeDetailedEmail(): $keyCache'); + return _cacheClient.deleteItem(keyCache); + } + + Future> getAllDetailedEmails(AccountId accountId, UserName userName) async { + final detailedEmailCacheList = await _cacheClient.getListByTupleKey(accountId.asString, userName.value); + detailedEmailCacheList.sortByLatestTime(); + log('OpenedEmailCacheManager::getAllDetailedEmails():SIZE: ${detailedEmailCacheList.length}'); + return detailedEmailCacheList; + } + + Future storeOpenedEmail( + AccountId accountId, + UserName userName, + DetailedEmailHiveCache detailedEmailCache + ) async { + final listDetailedEmails = await getAllDetailedEmails(accountId, userName); + + if (listDetailedEmails.length >= CachingConstants.maxNumberOpenedEmailsForOffline) { + final lastElementsListEmail = listDetailedEmails.sublist(CachingConstants.maxNumberOpenedEmailsForOffline - 1); + for (var email in lastElementsListEmail) { + if (email.emailContentPath != null) { + await _deleteFileExisted(email.emailContentPath!); + } + await removeDetailedEmail(accountId, userName, email.emailId); + } + } + await insertDetailedEmail(accountId, userName, detailedEmailCache); + + return detailedEmailCache; + } + + Future getStoredOpenedEmail( + AccountId accountId, + UserName userName, + EmailId emailId + ) async { + final keyCache = TupleKey(emailId.asString, accountId.asString, userName.value).encodeKey; + final detailedEmailCache = await _cacheClient.getItem(keyCache, needToReopen: true); + if (detailedEmailCache != null) { + return detailedEmailCache; + } else { + throw NotFoundStoredOpenedEmailException(); + } + } + + Future _deleteFileExisted(String pathFile) async { + await _fileUtils.deleteFile(pathFile); + } +} \ No newline at end of file diff --git a/lib/features/offline_mode/manager/opened_email_cache_worker_queue.dart b/lib/features/offline_mode/manager/opened_email_cache_worker_queue.dart new file mode 100644 index 0000000000..2712cc8106 --- /dev/null +++ b/lib/features/offline_mode/manager/opened_email_cache_worker_queue.dart @@ -0,0 +1,7 @@ +import 'package:tmail_ui_user/features/offline_mode/hive_worker/hive_worker_queue.dart'; + +class OpenedEmailCacheWorkerQueue extends WorkerQueue { + + @override + String get workerName => 'OpenedEmailCache'; +} \ No newline at end of file diff --git a/lib/features/offline_mode/manager/sending_email_cache_manager.dart b/lib/features/offline_mode/manager/sending_email_cache_manager.dart new file mode 100644 index 0000000000..34c6fecb51 --- /dev/null +++ b/lib/features/offline_mode/manager/sending_email_cache_manager.dart @@ -0,0 +1,104 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:tmail_ui_user/features/caching/clients/sending_email_hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; +import 'package:tmail_ui_user/features/offline_mode/extensions/list_sending_email_hive_cache_extension.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_email_hive_cache.dart'; +import 'package:tmail_ui_user/features/sending_queue/data/exceptions/sending_queue_exceptions.dart'; + +class SendingEmailCacheManager { + + final SendingEmailHiveCacheClient _hiveCacheClient; + + SendingEmailCacheManager(this._hiveCacheClient); + + Future storeSendingEmail( + AccountId accountId, + UserName userName, + SendingEmailHiveCache sendingEmailHiveCache + ) async { + final keyCache = TupleKey(sendingEmailHiveCache.sendingId, accountId.asString, userName.value).encodeKey; + await _hiveCacheClient.insertItem(keyCache, sendingEmailHiveCache); + final newSendingEmailHiveCache = await _hiveCacheClient.getItem(keyCache); + if (newSendingEmailHiveCache != null) { + return newSendingEmailHiveCache; + } else { + throw NotFoundSendingEmailHiveObject(); + } + } + + Future> getAllSendingEmailsByTupleKey(AccountId accountId, UserName userName) async { + final sendingEmailsCache = await _hiveCacheClient.getListByTupleKey(accountId.asString, userName.value); + sendingEmailsCache.sortByLatestTime(); + return sendingEmailsCache; + } + + Future deleteSendingEmail(AccountId accountId, UserName userName, String sendingId) async { + final keyCache = TupleKey(sendingId, accountId.asString, userName.value).encodeKey; + await _hiveCacheClient.deleteItem(keyCache); + final storedSendingEmail = await _hiveCacheClient.getItem(keyCache); + if (storedSendingEmail != null) { + throw ExistSendingEmailHiveObject(); + } + } + + Future> getAllSendingEmails() async { + final sendingEmailsCache = await _hiveCacheClient.getAll(); + sendingEmailsCache.sortByLatestTime(); + return sendingEmailsCache; + } + + Future clearAllSendingEmails() => _hiveCacheClient.clearAllData(); + + Future updateSendingEmail( + AccountId accountId, + UserName userName, + SendingEmailHiveCache sendingEmailHiveCache + ) async { + final keyCache = TupleKey(sendingEmailHiveCache.sendingId, accountId.asString, userName.value).encodeKey; + await _hiveCacheClient.updateItem(keyCache, sendingEmailHiveCache); + final newSendingEmailHiveCache = await _hiveCacheClient.getItem(keyCache); + if (newSendingEmailHiveCache != null) { + return newSendingEmailHiveCache; + } else { + throw NotFoundSendingEmailHiveObject(); + } + } + + Future> updateMultipleSendingEmail( + AccountId accountId, + UserName userName, + List listSendingEmailHiveCache + ) async { + final mapSendingEmailCache = { + for (var sendingEmailCache in listSendingEmailHiveCache) + TupleKey(sendingEmailCache.sendingId, accountId.asString, userName.value).encodeKey: sendingEmailCache + }; + await _hiveCacheClient.updateMultipleItem(mapSendingEmailCache); + final newListSendingEmailCache = await _hiveCacheClient.getValuesByListKey(mapSendingEmailCache.keys.toList()); + return newListSendingEmailCache; + } + + Future deleteMultipleSendingEmail(AccountId accountId, UserName userName, List sendingIds) async { + final listTupleKey = sendingIds.map((sendingId) => TupleKey(sendingId, accountId.asString, userName.value).encodeKey).toList(); + await _hiveCacheClient.deleteMultipleItem(listTupleKey); + final newListSendingEmailCache = await _hiveCacheClient.getValuesByListKey(listTupleKey); + if (newListSendingEmailCache.isNotEmpty) { + throw ExistSendingEmailHiveObject(); + } + } + + Future getStoredSendingEmail(AccountId accountId, UserName userName, String sendingId) async { + final keyCache = TupleKey(sendingId, accountId.asString, userName.value).encodeKey; + final storedSendingEmail = await _hiveCacheClient.getItem(keyCache); + if (storedSendingEmail != null) { + return storedSendingEmail; + } else { + throw NotFoundSendingEmailHiveObject(); + } + } + + Future closeSendingEmailHiveCacheBox() => _hiveCacheClient.closeBox(); +} \ No newline at end of file diff --git a/lib/features/offline_mode/model/attachment_hive_cache.dart b/lib/features/offline_mode/model/attachment_hive_cache.dart new file mode 100644 index 0000000000..2d0985d8a5 --- /dev/null +++ b/lib/features/offline_mode/model/attachment_hive_cache.dart @@ -0,0 +1,52 @@ + +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; + +part 'attachment_hive_cache.g.dart'; + +@HiveType(typeId: CachingConstants.ATTACHMENT_HIVE_CACHE_ID) +class AttachmentHiveCache extends HiveObject with EquatableMixin { + + @HiveField(0) + final String? partId; + + @HiveField(1) + final String? blobId; + + @HiveField(2) + final int? size; + + @HiveField(3) + final String? name; + + @HiveField(4) + final String? type; + + @HiveField(5) + final String? cid; + + @HiveField(6) + final String? disposition; + + AttachmentHiveCache({ + this.partId, + this.blobId, + this.size, + this.name, + this.type, + this.cid, + this.disposition + }); + + @override + List get props => [ + partId, + blobId, + size, + name, + type, + cid, + disposition + ]; +} \ No newline at end of file diff --git a/lib/features/offline_mode/model/detailed_email_hive_cache.dart b/lib/features/offline_mode/model/detailed_email_hive_cache.dart new file mode 100644 index 0000000000..f15f266db6 --- /dev/null +++ b/lib/features/offline_mode/model/detailed_email_hive_cache.dart @@ -0,0 +1,60 @@ + +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/email_header_hive_cache.dart'; + +import 'attachment_hive_cache.dart'; + +part 'detailed_email_hive_cache.g.dart'; + +@HiveType(typeId: CachingConstants.DETAILED_EMAIL_HIVE_CACHE_ID) +class DetailedEmailHiveCache extends HiveObject with EquatableMixin { + + @HiveField(0) + final String emailId; + + @HiveField(1) + final DateTime timeSaved; + + @HiveField(2) + final List? attachments; + + @HiveField(3) + final String? emailContentPath; + + @HiveField(4) + final List? headers; + + @HiveField(5) + final Map? keywords; + + @HiveField(6) + final List? messageId; + + @HiveField(7) + final List? references; + + DetailedEmailHiveCache({ + required this.emailId, + required this.timeSaved, + this.attachments, + this.emailContentPath, + this.headers, + this.keywords, + this.messageId, + this.references, + }); + + @override + List get props => [ + emailId, + timeSaved, + attachments, + emailContentPath, + headers, + keywords, + messageId, + references, + ]; +} \ No newline at end of file diff --git a/lib/features/offline_mode/model/email_header_hive_cache.dart b/lib/features/offline_mode/model/email_header_hive_cache.dart new file mode 100644 index 0000000000..09db0f6e63 --- /dev/null +++ b/lib/features/offline_mode/model/email_header_hive_cache.dart @@ -0,0 +1,21 @@ + +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; + +part 'email_header_hive_cache.g.dart'; + +@HiveType(typeId: CachingConstants.EMAIL_HEADER_HIVE_CACHE_ID) +class EmailHeaderHiveCache extends HiveObject with EquatableMixin { + + @HiveField(0) + final String name; + + @HiveField(1) + final String value; + + EmailHeaderHiveCache({required this.name, required this.value}); + + @override + List get props => [name, value]; +} \ No newline at end of file diff --git a/lib/features/offline_mode/model/sending_email_hive_cache.dart b/lib/features/offline_mode/model/sending_email_hive_cache.dart new file mode 100644 index 0000000000..80a66dcddb --- /dev/null +++ b/lib/features/offline_mode/model/sending_email_hive_cache.dart @@ -0,0 +1,72 @@ + +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; + +part 'sending_email_hive_cache.g.dart'; + +@HiveType(typeId: CachingConstants.SENDING_EMAIL_HIVE_CACHE_ID) +class SendingEmailHiveCache extends HiveObject with EquatableMixin { + + @HiveField(0) + final String sendingId; + + @HiveField(1) + final String email; + + @HiveField(2) + final String emailActionType; + + @HiveField(3) + final DateTime createTime; + + @HiveField(4) + final String? sentMailboxId; + + @HiveField(5) + final String? emailIdDestroyed; + + @HiveField(6) + final String? emailIdAnsweredOrForwarded; + + @HiveField(7) + final String? identityId; + + @HiveField(8) + final String? mailboxNameRequest; + + @HiveField(9) + final String? creationIdRequest; + + @HiveField(10) + final String sendingState; + + SendingEmailHiveCache( + this.sendingId, + this.email, + this.emailActionType, + this.createTime, + this.sentMailboxId, + this.emailIdDestroyed, + this.emailIdAnsweredOrForwarded, + this.identityId, + this.mailboxNameRequest, + this.creationIdRequest, + this.sendingState, + ); + + @override + List get props => [ + sendingId, + email, + emailActionType, + createTime, + sentMailboxId, + emailIdDestroyed, + emailIdAnsweredOrForwarded, + identityId, + mailboxNameRequest, + creationIdRequest, + sendingState, + ]; +} \ No newline at end of file diff --git a/lib/features/offline_mode/model/sending_state.dart b/lib/features/offline_mode/model/sending_state.dart new file mode 100644 index 0000000000..e9c11a6d0f --- /dev/null +++ b/lib/features/offline_mode/model/sending_state.dart @@ -0,0 +1,111 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +enum SendingState { + waiting, + running, + error, + canceled, + success; + + String getTitle(BuildContext context) { + switch(this) { + case SendingState.waiting: + case SendingState.running: + case SendingState.success: + return AppLocalizations.of(context).delivering; + case SendingState.canceled: + return AppLocalizations.of(context).canceled; + case SendingState.error: + return AppLocalizations.of(context).error; + } + } + + String getIcon(ImagePaths imagePaths) { + switch(this) { + case SendingState.waiting: + case SendingState.running: + case SendingState.success: + return imagePaths.icDelivering; + case SendingState.canceled: + return imagePaths.icDelivering; + case SendingState.error: + return imagePaths.icError; + } + } + + String getAvatarGroup(ImagePaths imagePaths) { + switch(this) { + case SendingState.running: + return imagePaths.icAvatarGroupDelivering; + case SendingState.waiting: + case SendingState.error: + case SendingState.success: + case SendingState.canceled: + return imagePaths.icAvatarGroup; + } + } + + String getAvatarPersonal(ImagePaths imagePaths) { + switch(this) { + case SendingState.running: + return imagePaths.icAvatarPersonalDelivering; + case SendingState.waiting: + case SendingState.error: + case SendingState.success: + case SendingState.canceled: + return imagePaths.icAvatarPersonal; + } + } + + Color getTitleColor() { + switch(this) { + case SendingState.waiting: + case SendingState.running: + case SendingState.success: + case SendingState.canceled: + return AppColor.colorTitleSendingItem; + case SendingState.error: + return AppColor.colorErrorState; + } + } + + Color getBackgroundColor() { + switch(this) { + case SendingState.waiting: + case SendingState.running: + case SendingState.success: + case SendingState.canceled: + return AppColor.colorBackgroundDeliveringState; + case SendingState.error: + return AppColor.colorBackgroundErrorState; + } + } + + Color getTitleSendingEmailItemColor() { + switch(this) { + case SendingState.running: + return AppColor.colorDeliveringState; + case SendingState.waiting: + case SendingState.error: + case SendingState.success: + case SendingState.canceled: + return Colors.black; + } + } + + Color getSubTitleSendingEmailItemColor() { + switch(this) { + case SendingState.running: + return AppColor.colorDeliveringState; + case SendingState.waiting: + case SendingState.error: + case SendingState.success: + case SendingState.canceled: + return AppColor.colorTitleSendingItem; + } + } +} \ No newline at end of file diff --git a/lib/features/offline_mode/work_manager/one_time_work_request.dart b/lib/features/offline_mode/work_manager/one_time_work_request.dart new file mode 100644 index 0000000000..f5aaf9c92b --- /dev/null +++ b/lib/features/offline_mode/work_manager/one_time_work_request.dart @@ -0,0 +1,30 @@ + +import 'package:tmail_ui_user/features/offline_mode/work_manager/work_request.dart'; +import 'package:workmanager/workmanager.dart'; + +/// A WorkRequest for non-repeating work. +class OneTimeWorkRequest extends WorkRequest { + OneTimeWorkRequest({ + required String uniqueId, + required String taskId, + String? tag, + Map? inputData, + Duration initialDelay = Duration.zero, + Duration backoffPolicyDelay = Duration.zero, + ExistingWorkPolicy? existingWorkPolicy, + BackoffPolicy? backoffPolicy, + OutOfQuotaPolicy? outOfQuotaPolicy, + Constraints? constraints + }) : super( + uniqueId: uniqueId, + taskId: taskId, + tag: tag, + inputData: inputData, + initialDelay: initialDelay, + backoffPolicyDelay: backoffPolicyDelay, + existingWorkPolicy: existingWorkPolicy, + backoffPolicy: backoffPolicy, + outOfQuotaPolicy: outOfQuotaPolicy, + constraints: constraints + ); +} \ No newline at end of file diff --git a/lib/features/offline_mode/work_manager/periodic_work_request.dart b/lib/features/offline_mode/work_manager/periodic_work_request.dart new file mode 100644 index 0000000000..a051a600e6 --- /dev/null +++ b/lib/features/offline_mode/work_manager/periodic_work_request.dart @@ -0,0 +1,36 @@ + +import 'package:tmail_ui_user/features/offline_mode/work_manager/work_request.dart'; +import 'package:workmanager/workmanager.dart'; + +/// A WorkRequest for repeating work. +class PeriodicWorkRequest extends WorkRequest { + final Duration? frequency; + + PeriodicWorkRequest({ + required String uniqueId, + required String taskId, + String? tag, + Map? inputData, + Duration initialDelay = Duration.zero, + Duration backoffPolicyDelay = Duration.zero, + ExistingWorkPolicy? existingWorkPolicy, + BackoffPolicy? backoffPolicy, + OutOfQuotaPolicy? outOfQuotaPolicy, + Constraints? constraints, + this.frequency + }) : super( + uniqueId: uniqueId, + taskId: taskId, + tag: tag, + inputData: inputData, + initialDelay: initialDelay, + backoffPolicyDelay: backoffPolicyDelay, + existingWorkPolicy: existingWorkPolicy, + backoffPolicy: backoffPolicy, + outOfQuotaPolicy: outOfQuotaPolicy, + constraints: constraints + ); + + @override + List get props => [super.props, frequency]; +} \ No newline at end of file diff --git a/lib/features/offline_mode/work_manager/sending_email_worker.dart b/lib/features/offline_mode/work_manager/sending_email_worker.dart new file mode 100644 index 0000000000..c46609cdca --- /dev/null +++ b/lib/features/offline_mode/work_manager/sending_email_worker.dart @@ -0,0 +1,247 @@ +import 'dart:async'; +import 'package:core/data/network/config/dynamic_url_interceptors.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:model/extensions/session_extension.dart'; +import 'package:model/oidc/token_oidc.dart'; +import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; +import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; +import 'package:tmail_ui_user/features/composer/domain/usecases/send_email_interactor.dart'; +import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_authenticated_account_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_credential_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_state.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/features/offline_mode/bindings/sending_email_interactor_bindings.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_state.dart'; +import 'package:tmail_ui_user/features/offline_mode/work_manager/worker.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/bindings/sending_queue_bindings.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/bindings/sending_queue_interactor_bindings.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart'; +import 'package:tmail_ui_user/features/session/domain/extensions/session_extensions.dart'; +import 'package:tmail_ui_user/features/session/domain/state/get_session_state.dart'; +import 'package:tmail_ui_user/features/session/domain/usecases/get_session_interactor.dart'; +import 'package:tmail_ui_user/main/bindings/main_bindings.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class SendingEmailWorker extends Worker { + + SendEmailInteractor? _sendEmailInteractor; + GetAuthenticatedAccountInteractor? _getAuthenticatedAccountInteractor; + DynamicUrlInterceptors? _dynamicUrlInterceptors; + AuthorizationInterceptors? _authorizationInterceptors; + GetSessionInteractor? _getSessionInteractor; + SendingQueueIsolateManager? _sendingQueueIsolateManager; + SendingEmailCacheManager? _sendingEmailCacheManager; + + late Completer _completer; + late SendingEmail _sendingEmail; + + AccountId? _currentAccountId; + Session? _currentSession; + + static SendingEmailWorker? _instance; + + SendingEmailWorker._(); + + factory SendingEmailWorker() => _instance ??= SendingEmailWorker._(); + + @override + Future doWork(String taskId, Map inputData) { + _completer = Completer(); + _sendingEmail = SendingEmail.fromJson(inputData); + log('SendingEmailObserver::observe():_sendingEmail: $_sendingEmail'); + _updatingSendingStateToMainUI( + sendingId: _sendingEmail.sendingId, + sendingState: SendingState.running + ); + _getAuthenticatedAccount(); + return _completer.future; + } + + @override + Future bindDI() async { + await Future.wait([ + MainBindings().dependencies(), + HiveCacheConfig().setUp() + ]); + + SendingQueueInteractorBindings().dependencies(); + SendEmailInteractorBindings().dependencies(); + SendingQueueBindings().dependencies(); + + _getInteractorBindings(); + + await _sendingEmailCacheManager?.closeSendingEmailHiveCacheBox(); + } + + @override + void handleFailureViewState(Failure failure) { + log('SendingEmailObserver::_handleFailureViewState(): $failure'); + if (failure is SendEmailFailure) { + _handleSendEmailFailure(failure); + } else if (failure is GetAuthenticatedAccountFailure || + failure is GetSessionFailure || + failure is GetStoredTokenOidcFailure || + failure is GetCredentialFailure) { + _handleWorkerTaskToRetry(); + } + } + + @override + void handleSuccessViewState(Success success) { + log('SendingEmailObserver::handleSuccessViewState(): $success'); + if (success is GetSessionSuccess) { + _handleGetSessionSuccess(success); + } else if (success is GetStoredTokenOidcSuccess) { + _handleGetAccountByOidcSuccess(success); + } else if (success is GetCredentialViewState) { + _handleGetAccountByBasicAuthSuccess(success); + } else if (success is SendEmailSuccess) { + _handleSendEmailSuccess(success); + } + } + + @override + void handleOnError(Object? error, StackTrace stackTrace) { + super.handleOnError(error, stackTrace); + _handleWorkerTaskToRetry(); + } + + void _getInteractorBindings() { + _getAuthenticatedAccountInteractor = getBinding(); + _dynamicUrlInterceptors = getBinding(); + _authorizationInterceptors = getBinding(); + _getSessionInteractor = getBinding(); + _sendEmailInteractor = getBinding(); + _sendingQueueIsolateManager = getBinding(); + _sendingEmailCacheManager = getBinding(); + } + + void _updatingSendingStateToMainUI({String? sendingId, SendingState? sendingState}) { + final eventAction = _generateEventAction( + sendingId ?? _sendingEmail.sendingId, + sendingState ?? _sendingEmail.sendingState, + accountId: _currentAccountId, + userName: _currentSession?.username + ); + log('SendingEmailObserver::_updatingSendingStateToMainUI():eventAction: $eventAction'); + _sendingQueueIsolateManager?.addEvent(eventAction); + } + + void _getAuthenticatedAccount() { + consumeState(_getAuthenticatedAccountInteractor!.execute()); + } + + void _getSessionAction() { + consumeState(_getSessionInteractor!.execute()); + } + + void _handleGetSessionSuccess(GetSessionSuccess success) async { + _currentSession = success.session; + _currentAccountId = success.session.personalAccount.accountId; + final apiUrl = success.session.getQualifiedApiUrl(baseUrl: _dynamicUrlInterceptors?.jmapUrl); + if (apiUrl.isNotEmpty && _currentSession != null && _currentAccountId != null) { + _dynamicUrlInterceptors?.changeBaseUrl(apiUrl); + _sendEmailAction(_currentAccountId!, _currentSession!); + } else { + _handleWorkerTaskToRetry(); + } + } + + void _handleGetAccountByBasicAuthSuccess(GetCredentialViewState credentialViewState) { + _dynamicUrlInterceptors?.setJmapUrl(credentialViewState.baseUrl.toString()); + _authorizationInterceptors?.setBasicAuthorization( + credentialViewState.userName.value, + credentialViewState.password.value, + ); + _dynamicUrlInterceptors?.changeBaseUrl(credentialViewState.baseUrl.toString()); + _getSessionAction(); + } + + void _sendEmailAction(AccountId accountId, Session session) { + consumeState( + _sendEmailInteractor!.execute( + session, + accountId, + _sendingEmail.toEmailRequest(), + mailboxRequest: _getMailboxRequest() + ) + ); + } + + CreateNewMailboxRequest? _getMailboxRequest() { + if (_sendingEmail.mailboxNameRequest != null && + _sendingEmail.creationIdRequest != null) { + return CreateNewMailboxRequest( + _sendingEmail.creationIdRequest!, + _sendingEmail.mailboxNameRequest!); + } else { + return null; + } + } + + void _handleGetAccountByOidcSuccess(GetStoredTokenOidcSuccess storedTokenOidcSuccess) { + _dynamicUrlInterceptors?.setJmapUrl(storedTokenOidcSuccess.baseUrl.toString()); + _authorizationInterceptors?.setTokenAndAuthorityOidc( + newToken: storedTokenOidcSuccess.tokenOidc.toToken(), + newConfig: storedTokenOidcSuccess.oidcConfiguration + ); + _dynamicUrlInterceptors?.changeBaseUrl(storedTokenOidcSuccess.baseUrl.toString()); + _getSessionAction(); + } + + void _handleSendEmailSuccess(SendEmailSuccess success) { + _handleWorkerTaskSuccess(); + } + + void _handleSendEmailFailure(SendEmailFailure failure) { + _handleWorkerTaskFailure(failure.exception); + } + + void _handleWorkerTaskToRetry() async { + _updatingSendingStateToMainUI(sendingState: SendingState.canceled); + log('SendingEmailObserver::_handleWorkerTaskToRetry():'); + await Future.delayed( + const Duration(milliseconds: 1000), + () => _completer.complete(false) + ); + } + + void _handleWorkerTaskFailure(dynamic error) async { + log('SendingEmailObserver::_handleWorkerTaskFailure():error: $error'); + _updatingSendingStateToMainUI(sendingState: SendingState.error); + await Future.delayed( + const Duration(milliseconds: 1000), + () => _completer.completeError(error) + ); + } + + void _handleWorkerTaskSuccess() async { + log('SendingEmailObserver::_handleWorkerTaskSuccess():'); + _updatingSendingStateToMainUI(sendingState: SendingState.success); + await Future.delayed( + const Duration(milliseconds: 1000), + () => _completer.complete(true) + ); + } + + String _generateEventAction( + String sendingId, + SendingState sendingState, + { + AccountId? accountId, + UserName? userName + } + ) => TupleKey(sendingId, sendingState.name, accountId?.asString, userName?.value).toString(); +} \ No newline at end of file diff --git a/lib/features/offline_mode/work_manager/work_dispatcher.dart b/lib/features/offline_mode/work_manager/work_dispatcher.dart new file mode 100644 index 0000000000..82a33b5324 --- /dev/null +++ b/lib/features/offline_mode/work_manager/work_dispatcher.dart @@ -0,0 +1,8 @@ + +import 'package:tmail_ui_user/features/offline_mode/controller/work_manager_controller.dart'; +import 'package:workmanager/workmanager.dart'; + +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask(WorkManagerController().handleBackgroundTask); +} diff --git a/lib/features/offline_mode/work_manager/work_request.dart b/lib/features/offline_mode/work_manager/work_request.dart new file mode 100644 index 0000000000..e6be561135 --- /dev/null +++ b/lib/features/offline_mode/work_manager/work_request.dart @@ -0,0 +1,44 @@ + +import 'package:equatable/equatable.dart'; +import 'package:workmanager/workmanager.dart'; + +/// Represents the scheduling of requests +abstract class WorkRequest with EquatableMixin { + final String uniqueId; + final String taskId; + final String? tag; + final Map? inputData; + final Duration initialDelay; + final Duration backoffPolicyDelay; + final ExistingWorkPolicy? existingWorkPolicy; + final BackoffPolicy? backoffPolicy; + final OutOfQuotaPolicy? outOfQuotaPolicy; + final Constraints? constraints; + + WorkRequest( { + required this.uniqueId, + required this.taskId, + this.tag, + this.inputData, + this.initialDelay = Duration.zero, + this.backoffPolicyDelay = Duration.zero, + this.existingWorkPolicy, + this.backoffPolicy, + this.outOfQuotaPolicy, + this.constraints + }); + + @override + List get props => [ + uniqueId, + taskId, + tag, + inputData, + initialDelay, + backoffPolicyDelay, + existingWorkPolicy, + backoffPolicy, + outOfQuotaPolicy, + constraints + ]; +} \ No newline at end of file diff --git a/lib/features/offline_mode/work_manager/worker.dart b/lib/features/offline_mode/work_manager/worker.dart new file mode 100644 index 0000000000..e74755b5d8 --- /dev/null +++ b/lib/features/offline_mode/work_manager/worker.dart @@ -0,0 +1,30 @@ +import 'dart:async'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; + +abstract class Worker { + Future bindDI(); + + Future doWork(String taskId, Map inputData); + + void consumeState(Stream> newStateStream) { + newStateStream.listen( + _handleStateStream, + onError: handleOnError + ); + } + + void _handleStateStream(Either newState) { + newState.fold(handleFailureViewState, handleSuccessViewState); + } + + void handleFailureViewState(Failure failure) {} + + void handleSuccessViewState(Success success) {} + + void handleOnError(Object? error, StackTrace stackTrace) { + logError('WorkObserver::handleOnError():error: $error | stackTrace: $stackTrace'); + } +} \ No newline at end of file diff --git a/lib/features/offline_mode/work_manager/worker_type.dart b/lib/features/offline_mode/work_manager/worker_type.dart new file mode 100644 index 0000000000..c9647792a6 --- /dev/null +++ b/lib/features/offline_mode/work_manager/worker_type.dart @@ -0,0 +1,22 @@ + +import 'package:tmail_ui_user/features/offline_mode/config/work_manager_constants.dart'; +import 'package:tmail_ui_user/features/offline_mode/work_manager/sending_email_worker.dart'; +import 'package:tmail_ui_user/features/offline_mode/work_manager/worker.dart'; + +enum WorkerType { + sendingEmail; + + Worker getWorker() { + switch(this) { + case WorkerType.sendingEmail: + return SendingEmailWorker(); + } + } + + String get iOSUniqueId { + switch(this) { + case WorkerType.sendingEmail: + return WorkManagerConstants.sendingEmailUniqueId; + } + } +} \ No newline at end of file diff --git a/lib/features/push_notification/data/datasource/fcm_datasource.dart b/lib/features/push_notification/data/datasource/fcm_datasource.dart index 38efcca758..5ccdbb9535 100644 --- a/lib/features/push_notification/data/datasource/fcm_datasource.dart +++ b/lib/features/push_notification/data/datasource/fcm_datasource.dart @@ -1,16 +1,18 @@ import 'package:fcm/model/firebase_subscription.dart'; import 'package:fcm/model/type_name.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/push_notification/data/model/fcm_subscription.dart'; import 'package:tmail_ui_user/features/push_notification/domain/model/register_new_token_request.dart'; abstract class FCMDatasource { - Future storeStateToRefresh(TypeName typeName, jmap.State newState); + Future storeStateToRefresh(AccountId accountId, UserName userName, TypeName typeName, jmap.State newState); - Future getStateToRefresh(TypeName typeName); + Future getStateToRefresh(AccountId accountId, UserName userName, TypeName typeName); - Future deleteStateToRefresh(TypeName typeName); + Future deleteStateToRefresh(AccountId accountId, UserName userName, TypeName typeName); Future storeSubscription(FCMSubscriptionCache fcmSubscriptionCache); diff --git a/lib/features/push_notification/data/datasource_impl/cache_fcm_datasource_impl.dart b/lib/features/push_notification/data/datasource_impl/cache_fcm_datasource_impl.dart index 889a8421dc..4ba5d92ca0 100644 --- a/lib/features/push_notification/data/datasource_impl/cache_fcm_datasource_impl.dart +++ b/lib/features/push_notification/data/datasource_impl/cache_fcm_datasource_impl.dart @@ -1,6 +1,8 @@ import 'package:fcm/model/firebase_subscription.dart'; import 'package:fcm/model/type_name.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/push_notification/data/datasource/fcm_datasource.dart'; import 'package:tmail_ui_user/features/push_notification/data/local/fcm_cache_manager.dart'; import 'package:tmail_ui_user/features/push_notification/data/model/fcm_subscription.dart'; @@ -15,30 +17,24 @@ class CacheFCMDatasourceImpl extends FCMDatasource { CacheFCMDatasourceImpl(this._firebaseCacheManager, this._exceptionThrower); @override - Future storeStateToRefresh(TypeName typeName, jmap.State newState) { + Future storeStateToRefresh(AccountId accountId, UserName userName, TypeName typeName, jmap.State newState) { return Future.sync(() async { - return await _firebaseCacheManager.storeStateToRefresh(typeName, newState); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await _firebaseCacheManager.storeStateToRefresh(accountId, userName, typeName, newState); + }).catchError(_exceptionThrower.throwException); } @override - Future getStateToRefresh(TypeName typeName) { + Future getStateToRefresh(AccountId accountId,UserName userName, TypeName typeName) { return Future.sync(() async { - return await _firebaseCacheManager.getStateToRefresh(typeName); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await _firebaseCacheManager.getStateToRefresh(accountId, userName, typeName); + }).catchError(_exceptionThrower.throwException); } @override - Future deleteStateToRefresh(TypeName typeName) { + Future deleteStateToRefresh(AccountId accountId, UserName userName, TypeName typeName) { return Future.sync(() async { - return await _firebaseCacheManager.deleteStateToRefresh(typeName); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + return await _firebaseCacheManager.deleteStateToRefresh(accountId, userName, typeName); + }).catchError(_exceptionThrower.throwException); } @override @@ -55,18 +51,14 @@ class CacheFCMDatasourceImpl extends FCMDatasource { Future storeSubscription(FCMSubscriptionCache fcmSubscriptionCache) { return Future.sync(() async { return await _firebaseCacheManager.storeSubscription(fcmSubscriptionCache); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future geSubscription() { return Future.sync(() async { return await _firebaseCacheManager.getSubscription(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override diff --git a/lib/features/push_notification/data/datasource_impl/fcm_datasource_impl.dart b/lib/features/push_notification/data/datasource_impl/fcm_datasource_impl.dart index d6a80a1fe9..9d8e4741dd 100644 --- a/lib/features/push_notification/data/datasource_impl/fcm_datasource_impl.dart +++ b/lib/features/push_notification/data/datasource_impl/fcm_datasource_impl.dart @@ -1,6 +1,8 @@ import 'package:fcm/model/firebase_subscription.dart'; import 'package:fcm/model/type_name.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/push_notification/data/datasource/fcm_datasource.dart'; import 'package:tmail_ui_user/features/push_notification/data/model/fcm_subscription.dart'; import 'package:tmail_ui_user/features/push_notification/data/network/fcm_api.dart'; @@ -19,23 +21,21 @@ class FcmDatasourceImpl extends FCMDatasource { Future getFirebaseSubscriptionByDeviceId(String deviceId) { return Future.sync(() async { return await _fcmApi.getFirebaseSubscriptionByDeviceId(deviceId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future deleteStateToRefresh(TypeName typeName) { + Future deleteStateToRefresh(AccountId accountId, UserName userName, TypeName typeName) { throw UnimplementedError(); } @override - Future getStateToRefresh(TypeName typeName) { + Future getStateToRefresh(AccountId accountId, UserName userName, TypeName typeName) { throw UnimplementedError(); } @override - Future storeStateToRefresh(TypeName typeName, jmap.State newState) { + Future storeStateToRefresh(AccountId accountId, UserName userName, TypeName typeName, jmap.State newState) { throw UnimplementedError(); } @@ -44,9 +44,7 @@ class FcmDatasourceImpl extends FCMDatasource { return Future.sync(() async { final firebaseSubscription = await _fcmApi.registerNewToken(newTokenRequest); return firebaseSubscription.fromDeviceId(newDeviceId: newTokenRequest.firebaseSubscription.deviceClientId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override @@ -63,8 +61,6 @@ class FcmDatasourceImpl extends FCMDatasource { Future destroySubscription(String subscriptionId) { return Future.sync(() async { return await _fcmApi.destroySubscription(subscriptionId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/push_notification/data/local/fcm_cache_manager.dart b/lib/features/push_notification/data/local/fcm_cache_manager.dart index 49ef77d656..b916cb7eb5 100644 --- a/lib/features/push_notification/data/local/fcm_cache_manager.dart +++ b/lib/features/push_notification/data/local/fcm_cache_manager.dart @@ -1,27 +1,28 @@ -import 'package:core/utils/app_logger.dart'; import 'package:fcm/model/type_name.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:tmail_ui_user/features/caching/subscription_cache_client.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:tmail_ui_user/features/caching/clients/fcm_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/subscription_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; import 'package:tmail_ui_user/features/push_notification/data/model/fcm_subscription.dart'; import 'package:tmail_ui_user/features/push_notification/domain/exceptions/fcm_exception.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; class FCMCacheManager { - final SharedPreferences _sharedPreferences; + final FcmCacheClient _fcmCacheClient; final FCMSubscriptionCacheClient _fcmSubscriptionCacheClient; + FCMCacheManager(this._fcmCacheClient,this._fcmSubscriptionCacheClient); - FCMCacheManager(this._sharedPreferences,this._fcmSubscriptionCacheClient); - - Future storeStateToRefresh(TypeName typeName, jmap.State newState) { - return _sharedPreferences.setString(typeName.value, newState.value); + Future storeStateToRefresh(AccountId accountId, UserName userName, TypeName typeName, jmap.State newState) { + final stateKeyCache = TupleKey(typeName.value, accountId.asString, userName.value).encodeKey; + return _fcmCacheClient.insertItem(stateKeyCache, newState.value); } - Future getStateToRefresh(TypeName typeName) async { - log('FCMCacheManager::getStoredFcmStateChange():keys_BEFORE: ${_sharedPreferences.getKeys().toString()}'); - await _sharedPreferences.reload(); - log('FCMCacheManager::getStoredFcmStateChange():keys_AFTER: ${_sharedPreferences.getKeys().toString()}'); - final stateValue = _sharedPreferences.getString(typeName.value); + Future getStateToRefresh(AccountId accountId, UserName userName, TypeName typeName) async { + final stateKeyCache = TupleKey(typeName.value, accountId.asString, userName.value).encodeKey; + final stateValue = await _fcmCacheClient.getItem(stateKeyCache); if (stateValue != null) { return jmap.State(stateValue); } else { @@ -33,16 +34,13 @@ class FCMCacheManager { } } - Future deleteStateToRefresh(TypeName typeName) { - return _sharedPreferences.remove(typeName.value); + Future deleteStateToRefresh(AccountId accountId, UserName userName, TypeName typeName) { + final stateKeyCache = TupleKey(typeName.value, accountId.asString, userName.value).encodeKey; + return _fcmCacheClient.deleteItem(stateKeyCache); } - Future clearAllStateToRefresh() async { - return await Future.wait([ - _sharedPreferences.remove(TypeName.emailType.value), - _sharedPreferences.remove(TypeName.mailboxType.value), - _sharedPreferences.remove(TypeName.emailDelivery.value), - ]).then((listResult) => listResult.every((result) => result)); + Future clearAllStateToRefresh() async { + return _fcmCacheClient.clearAllData(); } Future storeSubscription(FCMSubscriptionCache fcmSubscriptionCache) { @@ -51,11 +49,11 @@ class FCMCacheManager { } Future getSubscription() async { - final _fcmSubscription = await _fcmSubscriptionCacheClient.getItem(FCMSubscriptionCache.keyCacheValue); - if (_fcmSubscription == null) { + final fcmSubscription = await _fcmSubscriptionCacheClient.getItem(FCMSubscriptionCache.keyCacheValue); + if (fcmSubscription == null) { throw NotFoundSubscriptionException(); } else { - return _fcmSubscription; + return fcmSubscription; } } } \ No newline at end of file diff --git a/lib/features/push_notification/data/model/fcm_subscription.dart b/lib/features/push_notification/data/model/fcm_subscription.dart index aaef3e9366..b7cb0aa263 100644 --- a/lib/features/push_notification/data/model/fcm_subscription.dart +++ b/lib/features/push_notification/data/model/fcm_subscription.dart @@ -5,7 +5,7 @@ import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; part 'fcm_subscription.g.dart'; -@HiveType(typeId: CachingConstants.FCM_SUBSCRIPTION_HIVE_CACHE_INDENTITY) +@HiveType(typeId: CachingConstants.FCM_SUBSCRIPTION_HIVE_CACHE_IDENTITY) class FCMSubscriptionCache extends HiveObject with EquatableMixin { static const String keyCacheValue = 'fcmSubscriptionCache'; diff --git a/lib/features/push_notification/data/repository/fcm_repository_impl.dart b/lib/features/push_notification/data/repository/fcm_repository_impl.dart index 92fbf7c020..338e909bbc 100644 --- a/lib/features/push_notification/data/repository/fcm_repository_impl.dart +++ b/lib/features/push_notification/data/repository/fcm_repository_impl.dart @@ -1,13 +1,26 @@ import 'package:core/data/model/source_type/data_source_type.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:fcm/model/firebase_subscription.dart'; import 'package:fcm/model/type_name.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; +import 'package:model/email/email_property.dart'; +import 'package:model/extensions/list_email_extension.dart'; +import 'package:model/extensions/mailbox_extension.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/push_notification/data/datasource/fcm_datasource.dart'; import 'package:tmail_ui_user/features/push_notification/data/extensions/fcm_subscription_extensions.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/exceptions/fcm_exception.dart'; import 'package:tmail_ui_user/features/push_notification/domain/model/fcm_subscription.dart'; import 'package:tmail_ui_user/features/push_notification/domain/model/register_new_token_request.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/utils/fcm_constants.dart'; import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; @@ -17,14 +30,17 @@ class FCMRepositoryImpl extends FCMRepository { final Map _fcmDatasource; final ThreadDataSource _threadDataSource; + final Map _mapMailboxDataSource; FCMRepositoryImpl( this._fcmDatasource, - this._threadDataSource + this._threadDataSource, + this._mapMailboxDataSource ); @override Future getEmailChangesToPushNotification( + Session session, AccountId accountId, jmap.State currentState, { @@ -38,6 +54,7 @@ class FCMRepositoryImpl extends FCMRepository { while (hasMoreChanges && sinceState != null) { final changesResponse = await _threadDataSource.getChanges( + session, accountId, sinceState, propertiesCreated: propertiesCreated, @@ -62,18 +79,18 @@ class FCMRepositoryImpl extends FCMRepository { } @override - Future storeStateToRefresh(TypeName typeName, jmap.State newState) { - return _fcmDatasource[DataSourceType.local]!.storeStateToRefresh(typeName, newState); + Future storeStateToRefresh(AccountId accountId, UserName userName, TypeName typeName, jmap.State newState) { + return _fcmDatasource[DataSourceType.local]!.storeStateToRefresh(accountId, userName, typeName, newState); } @override - Future getStateToRefresh(TypeName typeName) { - return _fcmDatasource[DataSourceType.local]!.getStateToRefresh(typeName); + Future getStateToRefresh(AccountId accountId, UserName userName, TypeName typeName) { + return _fcmDatasource[DataSourceType.local]!.getStateToRefresh(accountId, userName, typeName); } @override - Future deleteStateToRefresh(TypeName typeName) { - return _fcmDatasource[DataSourceType.local]!.deleteStateToRefresh(typeName); + Future deleteStateToRefresh(AccountId accountId, UserName userName, TypeName typeName) { + return _fcmDatasource[DataSourceType.local]!.deleteStateToRefresh(accountId, userName, typeName); } @override @@ -88,8 +105,8 @@ class FCMRepositoryImpl extends FCMRepository { @override Future getSubscription() async { - final _fcmSubScription = await _fcmDatasource[DataSourceType.local]!.geSubscription(); - return FCMSubscription(_fcmSubScription.deviceId, _fcmSubScription.subscriptionId); + final fcmSubScription = await _fcmDatasource[DataSourceType.local]!.geSubscription(); + return FCMSubscription(fcmSubScription.deviceId, fcmSubScription.subscriptionId); } @override @@ -101,4 +118,103 @@ class FCMRepositoryImpl extends FCMRepository { Future destroySubscription(String subscriptionId) { return _fcmDatasource[DataSourceType.network]!.destroySubscription(subscriptionId); } + + @override + Future> getMailboxesNotPutNotifications(Session session, AccountId accountId) async { + final mailboxesCache = await _mapMailboxDataSource[DataSourceType.local]!.getAllMailboxCache(accountId, session.username); + final mailboxesCacheNotPutNotifications = mailboxesCache + .map((mailbox) => mailbox.toPresentationMailbox()) + .where((presentationMailbox) => presentationMailbox.pushNotificationDeactivated) + .toList(); + log('FCMRepositoryImpl::getMailboxesNotPutNotifications():mailboxesCacheNotPutNotifications: $mailboxesCacheNotPutNotifications'); + if (mailboxesCacheNotPutNotifications.isNotEmpty && mailboxesCacheNotPutNotifications.length == FcmConstants.mailboxRuleAllowPushNotifications.length) { + return mailboxesCacheNotPutNotifications; + } else { + final mailboxResponse = await _mapMailboxDataSource[DataSourceType.network]!.getAllMailbox(session, accountId); + final mailboxes = mailboxResponse.mailboxes ?? []; + final mailboxesNotPutNotifications = mailboxes + .map((mailbox) => mailbox.toPresentationMailbox()) + .where((presentationMailbox) => presentationMailbox.pushNotificationDeactivated) + .toList(); + log('FCMRepositoryImpl::getMailboxesNotPutNotifications():mailboxesNotPutNotifications: $mailboxesNotPutNotifications'); + return mailboxesNotPutNotifications; + } + } + + @override + Future> getEmailChangesToRemoveNotification( + Session session, + AccountId accountId, + jmap.State currentState, + { + Properties? propertiesCreated, + Properties? propertiesUpdated + } + ) async { + EmailChangeResponse? emailChangeResponse; + bool hasMoreChanges = true; + jmap.State? sinceState = currentState; + + while (hasMoreChanges && sinceState != null) { + final changesResponse = await _threadDataSource.getChanges( + session, + accountId, + sinceState, + propertiesCreated: propertiesCreated, + propertiesUpdated: propertiesUpdated + ); + + hasMoreChanges = changesResponse.hasMoreChanges; + sinceState = changesResponse.newStateChanges; + + if (emailChangeResponse != null) { + emailChangeResponse.union(changesResponse); + } else { + emailChangeResponse = changesResponse; + } + } + + if (emailChangeResponse != null) { + final listEmailIdMarkAsRead = emailChangeResponse.updated + ?.where((email) => email.keywords?.containsKey(KeyWordIdentifier.emailSeen) == true) + .toList() + .listEmailIds ?? []; + final listEmailIdDestroyed = emailChangeResponse.destroyed ?? []; + log('FCMRepositoryImpl::getEmailChangesToRemoveNotification():listEmailIdMarkAsRead: $listEmailIdMarkAsRead | listEmailIdDestroyed: $listEmailIdDestroyed'); + final allListEmailIdsNeedRemove = listEmailIdMarkAsRead + listEmailIdDestroyed; + return allListEmailIdsNeedRemove; + } else { + return []; + } + } + + @override + Future> getNewReceiveEmailFromNotification(Session session, AccountId accountId, jmap.State currentState) async { + EmailChangeResponse? emailChangeResponse; + bool hasMoreChanges = true; + jmap.State? sinceState = currentState; + + while (hasMoreChanges && sinceState != null) { + final changesResponse = await _threadDataSource.getChanges( + session, + accountId, + sinceState, + propertiesCreated: Properties({EmailProperty.id})); + + hasMoreChanges = changesResponse.hasMoreChanges; + sinceState = changesResponse.newStateChanges; + + if (emailChangeResponse != null) { + emailChangeResponse.union(changesResponse); + } else { + emailChangeResponse = changesResponse; + } + } + + if (emailChangeResponse?.created?.isNotEmpty == true) { + return emailChangeResponse!.created!.listEmailIds; + } else { + throw NotFoundNewReceiveEmailException(); + } + } } \ No newline at end of file diff --git a/lib/features/push_notification/domain/exceptions/fcm_exception.dart b/lib/features/push_notification/domain/exceptions/fcm_exception.dart index 0ec2be9717..4be012173a 100644 --- a/lib/features/push_notification/domain/exceptions/fcm_exception.dart +++ b/lib/features/push_notification/domain/exceptions/fcm_exception.dart @@ -8,4 +8,10 @@ class NotFoundEmailDeliveryStateException implements Exception {} class NotFoundFirebaseSubscriptionException implements Exception {} -class NotFoundSubscriptionException implements Exception {} \ No newline at end of file +class NotFoundSubscriptionException implements Exception {} + +class NotFoundEmailStateException implements Exception {} + +class NotFoundNewReceiveEmailException implements Exception {} + +class EmailStateNoChangeException implements Exception {} \ No newline at end of file diff --git a/lib/features/push_notification/domain/repository/fcm_repository.dart b/lib/features/push_notification/domain/repository/fcm_repository.dart index 12bd75b663..bee2561ed9 100644 --- a/lib/features/push_notification/domain/repository/fcm_repository.dart +++ b/lib/features/push_notification/domain/repository/fcm_repository.dart @@ -2,6 +2,10 @@ import 'package:fcm/model/firebase_subscription.dart'; import 'package:fcm/model/type_name.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/features/push_notification/domain/model/fcm_subscription.dart'; import 'package:tmail_ui_user/features/push_notification/domain/model/register_new_token_request.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; @@ -9,6 +13,7 @@ import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; abstract class FCMRepository { Future getEmailChangesToPushNotification( + Session session, AccountId accountId, jmap.State currentState, { @@ -17,11 +22,11 @@ abstract class FCMRepository { } ); - Future storeStateToRefresh(TypeName typeName, jmap.State newState); + Future storeStateToRefresh(AccountId accountId, UserName userName, TypeName typeName, jmap.State newState); - Future getStateToRefresh(TypeName typeName); + Future getStateToRefresh(AccountId accountId, UserName userName, TypeName typeName); - Future deleteStateToRefresh(TypeName typeName); + Future deleteStateToRefresh(AccountId accountId, UserName userName, TypeName typeName); Future storeSubscription(FCMSubscription fcmSubscription); @@ -32,4 +37,18 @@ abstract class FCMRepository { Future getSubscription(); Future destroySubscription(String subscriptionId); + + Future> getMailboxesNotPutNotifications(Session session, AccountId accountId); + + Future> getEmailChangesToRemoveNotification( + Session session, + AccountId accountId, + jmap.State currentState, + { + Properties? propertiesCreated, + Properties? propertiesUpdated + } + ); + + Future> getNewReceiveEmailFromNotification(Session session, AccountId accountId, jmap.State currentState); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/delete_email_state_to_refresh_state.dart b/lib/features/push_notification/domain/state/delete_email_state_to_refresh_state.dart index 0850245b1e..da25563a60 100644 --- a/lib/features/push_notification/domain/state/delete_email_state_to_refresh_state.dart +++ b/lib/features/push_notification/domain/state/delete_email_state_to_refresh_state.dart @@ -13,10 +13,6 @@ class DeleteEmailStateToRefreshSuccess extends UIState { } class DeleteEmailStateToRefreshFailure extends FeatureFailure { - final dynamic exception; - DeleteEmailStateToRefreshFailure(this.exception); - - @override - List get props => [exception]; + DeleteEmailStateToRefreshFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/delete_mailbox_state_to_refresh_state.dart b/lib/features/push_notification/domain/state/delete_mailbox_state_to_refresh_state.dart index 7c6f8df654..e72c30dc9c 100644 --- a/lib/features/push_notification/domain/state/delete_mailbox_state_to_refresh_state.dart +++ b/lib/features/push_notification/domain/state/delete_mailbox_state_to_refresh_state.dart @@ -13,10 +13,6 @@ class DeleteMailboxStateToRefreshSuccess extends UIState { } class DeleteMailboxStateToRefreshFailure extends FeatureFailure { - final dynamic exception; - DeleteMailboxStateToRefreshFailure(this.exception); - - @override - List get props => [exception]; + DeleteMailboxStateToRefreshFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/destroy_subscription_state.dart b/lib/features/push_notification/domain/state/destroy_subscription_state.dart index d1de79b2c7..6868ef40bf 100644 --- a/lib/features/push_notification/domain/state/destroy_subscription_state.dart +++ b/lib/features/push_notification/domain/state/destroy_subscription_state.dart @@ -1,7 +1,6 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; - class DestroySubscriptionLoading extends UIState {} class DestroySubscriptionSuccess extends UIState { @@ -15,10 +14,6 @@ class DestroySubscriptionSuccess extends UIState { } class DestroySubscriptionFailure extends FeatureFailure { - final dynamic exception; - - DestroySubscriptionFailure(this.exception); - @override - List get props => [exception]; + DestroySubscriptionFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/get_email_changes_state.dart b/lib/features/push_notification/domain/state/get_email_changes_to_push_notification_state.dart similarity index 51% rename from lib/features/push_notification/domain/state/get_email_changes_state.dart rename to lib/features/push_notification/domain/state/get_email_changes_to_push_notification_state.dart index bb18aba644..2ec36d0f8e 100644 --- a/lib/features/push_notification/domain/state/get_email_changes_state.dart +++ b/lib/features/push_notification/domain/state/get_email_changes_to_push_notification_state.dart @@ -1,6 +1,8 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/email/presentation_email.dart'; class GetEmailChangesToPushNotificationLoading extends UIState {} @@ -8,18 +10,16 @@ class GetEmailChangesToPushNotificationLoading extends UIState {} class GetEmailChangesToPushNotificationSuccess extends UIState { final List emailList; + final AccountId accountId; + final UserName userName; - GetEmailChangesToPushNotificationSuccess(this.emailList); + GetEmailChangesToPushNotificationSuccess(this.accountId, this.userName, this.emailList); @override - List get props => [emailList]; + List get props => [accountId, userName, emailList]; } class GetEmailChangesToPushNotificationFailure extends FeatureFailure { - final dynamic exception; - GetEmailChangesToPushNotificationFailure(this.exception); - - @override - List get props => [exception]; + GetEmailChangesToPushNotificationFailure(exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/get_email_changes_to_remove_notification_state.dart b/lib/features/push_notification/domain/state/get_email_changes_to_remove_notification_state.dart new file mode 100644 index 0000000000..e4e552cf48 --- /dev/null +++ b/lib/features/push_notification/domain/state/get_email_changes_to_remove_notification_state.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; + +class GetEmailChangesToRemoveNotificationLoading extends UIState {} + +class GetEmailChangesToRemoveNotificationSuccess extends UIState { + + final List emailIds; + + GetEmailChangesToRemoveNotificationSuccess(this.emailIds); + + @override + List get props => [emailIds]; +} + +class GetEmailChangesToRemoveNotificationFailure extends FeatureFailure { + + GetEmailChangesToRemoveNotificationFailure(exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/get_email_state_to_refresh_state.dart b/lib/features/push_notification/domain/state/get_email_state_to_refresh_state.dart index ffe2b10596..b42787f4ce 100644 --- a/lib/features/push_notification/domain/state/get_email_state_to_refresh_state.dart +++ b/lib/features/push_notification/domain/state/get_email_state_to_refresh_state.dart @@ -16,10 +16,6 @@ class GetEmailStateToRefreshSuccess extends UIState { } class GetEmailStateToRefreshFailure extends FeatureFailure { - final dynamic exception; - GetEmailStateToRefreshFailure(this.exception); - - @override - List get props => [exception]; + GetEmailStateToRefreshFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/get_fcm_subscription_local.dart b/lib/features/push_notification/domain/state/get_fcm_subscription_local.dart index 1b274b1bdd..431783a0f9 100644 --- a/lib/features/push_notification/domain/state/get_fcm_subscription_local.dart +++ b/lib/features/push_notification/domain/state/get_fcm_subscription_local.dart @@ -16,10 +16,6 @@ class GetFCMSubscriptionLocalSuccess extends UIState { } class GetFCMSubscriptionLocalFailure extends FeatureFailure { - final dynamic exception; - GetFCMSubscriptionLocalFailure(this.exception); - - @override - List get props => [exception]; + GetFCMSubscriptionLocalFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/get_firebase_subscription_state.dart b/lib/features/push_notification/domain/state/get_firebase_subscription_state.dart index 1bc799cc47..c9d714d8d3 100644 --- a/lib/features/push_notification/domain/state/get_firebase_subscription_state.dart +++ b/lib/features/push_notification/domain/state/get_firebase_subscription_state.dart @@ -16,10 +16,6 @@ class GetFirebaseSubscriptionSuccess extends UIState { } class GetFirebaseSubscriptionFailure extends FeatureFailure { - final dynamic exception; - GetFirebaseSubscriptionFailure(this.exception); - - @override - List get props => [exception]; + GetFirebaseSubscriptionFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/get_mailbox_state_to_refresh_state.dart b/lib/features/push_notification/domain/state/get_mailbox_state_to_refresh_state.dart index f5adaff5dc..a834137fe9 100644 --- a/lib/features/push_notification/domain/state/get_mailbox_state_to_refresh_state.dart +++ b/lib/features/push_notification/domain/state/get_mailbox_state_to_refresh_state.dart @@ -16,10 +16,6 @@ class GetMailboxStateToRefreshSuccess extends UIState { } class GetMailboxStateToRefreshFailure extends FeatureFailure { - final dynamic exception; - GetMailboxStateToRefreshFailure(this.exception); - - @override - List get props => [exception]; + GetMailboxStateToRefreshFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/get_mailboxes_not_put_notifications_state.dart b/lib/features/push_notification/domain/state/get_mailboxes_not_put_notifications_state.dart new file mode 100644 index 0000000000..86e1f90ebd --- /dev/null +++ b/lib/features/push_notification/domain/state/get_mailboxes_not_put_notifications_state.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; + +class GetMailboxesNotPutNotificationsLoading extends UIState {} + +class GetMailboxesNotPutNotificationsSuccess extends UIState { + + final List mailboxes; + + GetMailboxesNotPutNotificationsSuccess(this.mailboxes); + + @override + List get props => [mailboxes]; +} + +class GetMailboxesNotPutNotificationsFailure extends FeatureFailure { + + GetMailboxesNotPutNotificationsFailure(exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/get_new_receive_email_from_notification_state.dart b/lib/features/push_notification/domain/state/get_new_receive_email_from_notification_state.dart new file mode 100644 index 0000000000..d0cc03e701 --- /dev/null +++ b/lib/features/push_notification/domain/state/get_new_receive_email_from_notification_state.dart @@ -0,0 +1,25 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; + +class GetNewReceiveEmailFromNotificationLoading extends UIState {} + +class GetNewReceiveEmailFromNotificationSuccess extends UIState { + + final Set emailIds; + final AccountId accountId; + final Session? session; + + GetNewReceiveEmailFromNotificationSuccess(this.accountId, this.session, this.emailIds); + + @override + List get props => [accountId, session, emailIds]; +} + +class GetNewReceiveEmailFromNotificationFailure extends FeatureFailure { + + GetNewReceiveEmailFromNotificationFailure(exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/get_stored_email_delivery_state.dart b/lib/features/push_notification/domain/state/get_stored_email_delivery_state.dart index dc12355e41..66156cc1cf 100644 --- a/lib/features/push_notification/domain/state/get_stored_email_delivery_state.dart +++ b/lib/features/push_notification/domain/state/get_stored_email_delivery_state.dart @@ -16,10 +16,6 @@ class GetStoredEmailDeliveryStateSuccess extends UIState { } class GetStoredEmailDeliveryStateFailure extends FeatureFailure { - final dynamic exception; - GetStoredEmailDeliveryStateFailure(this.exception); - - @override - List get props => [exception]; + GetStoredEmailDeliveryStateFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/register_new_token_state.dart b/lib/features/push_notification/domain/state/register_new_token_state.dart index 97f819be3a..522ddda99d 100644 --- a/lib/features/push_notification/domain/state/register_new_token_state.dart +++ b/lib/features/push_notification/domain/state/register_new_token_state.dart @@ -16,10 +16,6 @@ class RegisterNewTokenSuccess extends UIState { } class RegisterNewTokenFailure extends FeatureFailure { - final dynamic exception; - RegisterNewTokenFailure(this.exception); - - @override - List get props => [exception]; + RegisterNewTokenFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/store_email_delivery_state.dart b/lib/features/push_notification/domain/state/store_email_delivery_state.dart index 00bbbc1702..0c8c592409 100644 --- a/lib/features/push_notification/domain/state/store_email_delivery_state.dart +++ b/lib/features/push_notification/domain/state/store_email_delivery_state.dart @@ -4,19 +4,9 @@ import 'package:core/presentation/state/success.dart'; class StoreEmailDeliveryStateLoading extends UIState {} -class StoreEmailDeliveryStateSuccess extends UIState { - - StoreEmailDeliveryStateSuccess(); - - @override - List get props => []; -} +class StoreEmailDeliveryStateSuccess extends UIState {} class StoreEmailDeliveryStateFailure extends FeatureFailure { - final dynamic exception; - - StoreEmailDeliveryStateFailure(this.exception); - @override - List get props => [exception]; + StoreEmailDeliveryStateFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/store_email_state_to_refresh_state.dart b/lib/features/push_notification/domain/state/store_email_state_to_refresh_state.dart index 234394dea2..7c588ebfe2 100644 --- a/lib/features/push_notification/domain/state/store_email_state_to_refresh_state.dart +++ b/lib/features/push_notification/domain/state/store_email_state_to_refresh_state.dart @@ -4,19 +4,9 @@ import 'package:core/presentation/state/success.dart'; class StoreEmailStateToRefreshLoading extends UIState {} -class StoreEmailStateToRefreshSuccess extends UIState { - - StoreEmailStateToRefreshSuccess(); - - @override - List get props => []; -} +class StoreEmailStateToRefreshSuccess extends UIState {} class StoreEmailStateToRefreshFailure extends FeatureFailure { - final dynamic exception; - - StoreEmailStateToRefreshFailure(this.exception); - @override - List get props => [exception]; + StoreEmailStateToRefreshFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/store_mailbox_state_to_refresh_state.dart b/lib/features/push_notification/domain/state/store_mailbox_state_to_refresh_state.dart index e8f39f44c9..1c6039cbfa 100644 --- a/lib/features/push_notification/domain/state/store_mailbox_state_to_refresh_state.dart +++ b/lib/features/push_notification/domain/state/store_mailbox_state_to_refresh_state.dart @@ -4,19 +4,9 @@ import 'package:core/presentation/state/success.dart'; class StoreMailboxStateToRefreshLoading extends UIState {} -class StoreMailboxStateToRefreshSuccess extends UIState { - - StoreMailboxStateToRefreshSuccess(); - - @override - List get props => []; -} +class StoreMailboxStateToRefreshSuccess extends UIState {} class StoreMailboxStateToRefreshFailure extends FeatureFailure { - final dynamic exception; - - StoreMailboxStateToRefreshFailure(this.exception); - @override - List get props => [exception]; + StoreMailboxStateToRefreshFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/state/store_subscription_state.dart b/lib/features/push_notification/domain/state/store_subscription_state.dart index 341f57e9d9..de362f5f07 100644 --- a/lib/features/push_notification/domain/state/store_subscription_state.dart +++ b/lib/features/push_notification/domain/state/store_subscription_state.dart @@ -4,19 +4,9 @@ import 'package:core/presentation/state/success.dart'; class StoreSubscriptionLoading extends UIState {} -class StoreSubscriptionSuccess extends UIState { - - StoreSubscriptionSuccess(); - - @override - List get props => []; -} +class StoreSubscriptionSuccess extends UIState {} class StoreSubscriptionFailure extends FeatureFailure { - final dynamic exception; - - StoreSubscriptionFailure(this.exception); - @override - List get props => [exception]; + StoreSubscriptionFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/push_notification/domain/usecases/delete_email_state_to_refresh_interactor.dart b/lib/features/push_notification/domain/usecases/delete_email_state_to_refresh_interactor.dart index e5a992df9b..8de8873005 100644 --- a/lib/features/push_notification/domain/usecases/delete_email_state_to_refresh_interactor.dart +++ b/lib/features/push_notification/domain/usecases/delete_email_state_to_refresh_interactor.dart @@ -2,6 +2,8 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:fcm/model/type_name.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/delete_email_state_to_refresh_state.dart'; @@ -10,10 +12,10 @@ class DeleteEmailStateToRefreshInteractor { DeleteEmailStateToRefreshInteractor(this._fcmRepository); - Stream> execute() async* { + Stream> execute(AccountId accountId, UserName userName) async* { try { yield Right(DeleteEmailStateToRefreshLoading()); - await _fcmRepository.deleteStateToRefresh(TypeName.emailType); + await _fcmRepository.deleteStateToRefresh(accountId, userName, TypeName.emailType); yield Right(DeleteEmailStateToRefreshSuccess()); } catch (e) { yield Left(DeleteEmailStateToRefreshFailure(e)); diff --git a/lib/features/push_notification/domain/usecases/delete_mailbox_state_to_refresh_interactor.dart b/lib/features/push_notification/domain/usecases/delete_mailbox_state_to_refresh_interactor.dart index cc3c6db621..f2e8aed850 100644 --- a/lib/features/push_notification/domain/usecases/delete_mailbox_state_to_refresh_interactor.dart +++ b/lib/features/push_notification/domain/usecases/delete_mailbox_state_to_refresh_interactor.dart @@ -2,6 +2,8 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:fcm/model/type_name.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/delete_mailbox_state_to_refresh_state.dart'; @@ -10,10 +12,10 @@ class DeleteMailboxStateToRefreshInteractor { DeleteMailboxStateToRefreshInteractor(this._fcmRepository); - Stream> execute() async* { + Stream> execute(AccountId accountId, UserName userName) async* { try { yield Right(DeleteMailboxStateToRefreshLoading()); - await _fcmRepository.deleteStateToRefresh(TypeName.mailboxType); + await _fcmRepository.deleteStateToRefresh(accountId, userName, TypeName.mailboxType); yield Right(DeleteMailboxStateToRefreshSuccess()); } catch (e) { yield Left(DeleteMailboxStateToRefreshFailure(e)); diff --git a/lib/features/push_notification/domain/usecases/get_email_changes_to_push_notification_interactor.dart b/lib/features/push_notification/domain/usecases/get_email_changes_to_push_notification_interactor.dart index 57ff490ceb..d88d29e4fc 100644 --- a/lib/features/push_notification/domain/usecases/get_email_changes_to_push_notification_interactor.dart +++ b/lib/features/push_notification/domain/usecases/get_email_changes_to_push_notification_interactor.dart @@ -3,10 +3,12 @@ import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/extensions/email_extension.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; -import 'package:tmail_ui_user/features/push_notification/domain/state/get_email_changes_state.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/get_email_changes_to_push_notification_state.dart'; class GetEmailChangesToPushNotificationInteractor { final FCMRepository _fcmRepository; @@ -14,7 +16,9 @@ class GetEmailChangesToPushNotificationInteractor { GetEmailChangesToPushNotificationInteractor(this._fcmRepository); Stream> execute( + Session session, AccountId accountId, + UserName userName, jmap.State currentState, { Properties? propertiesCreated, @@ -25,6 +29,7 @@ class GetEmailChangesToPushNotificationInteractor { yield Right(GetEmailChangesToPushNotificationLoading()); final emailsResponse = await _fcmRepository.getEmailChangesToPushNotification( + session, accountId, currentState, propertiesCreated: propertiesCreated, @@ -34,7 +39,7 @@ class GetEmailChangesToPushNotificationInteractor { ?.map((email) => email.toPresentationEmail()) .toList() ?? List.empty(); - yield Right(GetEmailChangesToPushNotificationSuccess(presentationEmailList)); + yield Right(GetEmailChangesToPushNotificationSuccess(accountId, userName, presentationEmailList)); } catch (e) { yield Left(GetEmailChangesToPushNotificationFailure(e)); } diff --git a/lib/features/push_notification/domain/usecases/get_email_changes_to_remove_notification_interactor.dart b/lib/features/push_notification/domain/usecases/get_email_changes_to_remove_notification_interactor.dart new file mode 100644 index 0000000000..24b0173194 --- /dev/null +++ b/lib/features/push_notification/domain/usecases/get_email_changes_to_remove_notification_interactor.dart @@ -0,0 +1,49 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/exceptions/fcm_exception.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/get_email_changes_to_remove_notification_state.dart'; + +class GetEmailChangesToRemoveNotificationInteractor { + final FCMRepository _fcmRepository; + final EmailRepository _emailRepository; + + GetEmailChangesToRemoveNotificationInteractor(this._fcmRepository, this._emailRepository); + + Stream> execute( + Session session, + AccountId accountId, + jmap.State newState, + { + Properties? propertiesCreated, + Properties? propertiesUpdated + } + ) async* { + try { + yield Right(GetEmailChangesToRemoveNotificationLoading()); + + final currentState = await _emailRepository.getEmailState(session, accountId); + log('GetEmailChangesToRemoveNotificationInteractor::execute():currentState: $currentState'); + if (currentState != null) { + final emailIds = await _fcmRepository.getEmailChangesToRemoveNotification( + session, + accountId, + currentState, + propertiesCreated: propertiesCreated, + propertiesUpdated: propertiesUpdated); + yield Right(GetEmailChangesToRemoveNotificationSuccess(emailIds)); + } else { + yield Left(GetEmailChangesToRemoveNotificationFailure(NotFoundEmailStateException())); + } + } catch (e) { + yield Left(GetEmailChangesToRemoveNotificationFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/usecases/get_email_state_to_refresh_interactor.dart b/lib/features/push_notification/domain/usecases/get_email_state_to_refresh_interactor.dart index 0476ba7a62..26c9c270d2 100644 --- a/lib/features/push_notification/domain/usecases/get_email_state_to_refresh_interactor.dart +++ b/lib/features/push_notification/domain/usecases/get_email_state_to_refresh_interactor.dart @@ -2,6 +2,8 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:fcm/model/type_name.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/get_email_state_to_refresh_state.dart'; @@ -10,10 +12,10 @@ class GetEmailStateToRefreshInteractor { GetEmailStateToRefreshInteractor(this._fcmRepository); - Stream> execute() async* { + Stream> execute(AccountId accountId, UserName userName) async* { try { yield Right(GetEmailStateToRefreshLoading()); - final storedState = await _fcmRepository.getStateToRefresh(TypeName.emailType); + final storedState = await _fcmRepository.getStateToRefresh(accountId, userName, TypeName.emailType); yield Right(GetEmailStateToRefreshSuccess(storedState)); } catch (e) { yield Left(GetEmailStateToRefreshFailure(e)); diff --git a/lib/features/push_notification/domain/usecases/get_fcm_subscription_local_interactor.dart b/lib/features/push_notification/domain/usecases/get_fcm_subscription_local_interactor.dart index e045f7cedc..43f9e20367 100644 --- a/lib/features/push_notification/domain/usecases/get_fcm_subscription_local_interactor.dart +++ b/lib/features/push_notification/domain/usecases/get_fcm_subscription_local_interactor.dart @@ -12,8 +12,8 @@ class GetFCMSubscriptionLocalInteractor { Stream> execute() async* { try { yield Right(GetFCMSubscriptionLocalLoading()); - final _subscription = await _fcmRepository.getSubscription(); - yield Right(GetFCMSubscriptionLocalSuccess(_subscription)); + final subscription = await _fcmRepository.getSubscription(); + yield Right(GetFCMSubscriptionLocalSuccess(subscription)); } catch (e) { yield Left(GetFCMSubscriptionLocalFailure(e)); } diff --git a/lib/features/push_notification/domain/usecases/get_mailbox_state_to_refresh_interactor.dart b/lib/features/push_notification/domain/usecases/get_mailbox_state_to_refresh_interactor.dart index 28d8e30d3a..30befb5bc7 100644 --- a/lib/features/push_notification/domain/usecases/get_mailbox_state_to_refresh_interactor.dart +++ b/lib/features/push_notification/domain/usecases/get_mailbox_state_to_refresh_interactor.dart @@ -2,6 +2,8 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:fcm/model/type_name.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/get_mailbox_state_to_refresh_state.dart'; @@ -10,10 +12,10 @@ class GetMailboxStateToRefreshInteractor { GetMailboxStateToRefreshInteractor(this._fcmRepository); - Stream> execute() async* { + Stream> execute(AccountId accountId, UserName userName) async* { try { yield Right(GetMailboxStateToRefreshLoading()); - final storedState = await _fcmRepository.getStateToRefresh(TypeName.mailboxType); + final storedState = await _fcmRepository.getStateToRefresh(accountId, userName, TypeName.mailboxType); yield Right(GetMailboxStateToRefreshSuccess(storedState)); } catch (e) { yield Left(GetMailboxStateToRefreshFailure(e)); diff --git a/lib/features/push_notification/domain/usecases/get_mailboxes_not_put_notifications_interactor.dart b/lib/features/push_notification/domain/usecases/get_mailboxes_not_put_notifications_interactor.dart new file mode 100644 index 0000000000..b953a2d118 --- /dev/null +++ b/lib/features/push_notification/domain/usecases/get_mailboxes_not_put_notifications_interactor.dart @@ -0,0 +1,23 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/get_mailboxes_not_put_notifications_state.dart'; + +class GetMailboxesNotPutNotificationsInteractor { + final FCMRepository _fcmRepository; + + GetMailboxesNotPutNotificationsInteractor(this._fcmRepository); + + Stream> execute(Session session, AccountId accountId) async* { + try { + yield Right(GetMailboxesNotPutNotificationsLoading()); + final mailboxes = await _fcmRepository.getMailboxesNotPutNotifications(session, accountId); + yield Right(GetMailboxesNotPutNotificationsSuccess(mailboxes)); + } catch (e) { + yield Left(GetMailboxesNotPutNotificationsFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/usecases/get_new_receive_email_from_notification_interactor.dart b/lib/features/push_notification/domain/usecases/get_new_receive_email_from_notification_interactor.dart new file mode 100644 index 0000000000..6af280620d --- /dev/null +++ b/lib/features/push_notification/domain/usecases/get_new_receive_email_from_notification_interactor.dart @@ -0,0 +1,43 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/exceptions/fcm_exception.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/get_new_receive_email_from_notification_state.dart'; + +class GetNewReceiveEmailFromNotificationInteractor { + final FCMRepository _fcmRepository; + final EmailRepository _emailRepository; + + GetNewReceiveEmailFromNotificationInteractor(this._fcmRepository, this._emailRepository); + + Stream> execute({ + required Session session, + required AccountId accountId, + required UserName userName, + required jmap.State newState + }) async* { + try { + yield Right(GetNewReceiveEmailFromNotificationLoading()); + + final currentState = await _emailRepository.getEmailState(session, accountId); + if (currentState != null && currentState != newState) { + final listEmailIds = await _fcmRepository.getNewReceiveEmailFromNotification( + session, + accountId, + currentState); + + yield Right(GetNewReceiveEmailFromNotificationSuccess(accountId, session, listEmailIds.toSet())); + } else { + yield Left(GetNewReceiveEmailFromNotificationFailure(EmailStateNoChangeException())); + } + } catch (e) { + yield Left(GetNewReceiveEmailFromNotificationFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/push_notification/domain/usecases/get_stored_email_delivery_state_interactor.dart b/lib/features/push_notification/domain/usecases/get_stored_email_delivery_state_interactor.dart index a14e8a98c4..5f20ad65ed 100644 --- a/lib/features/push_notification/domain/usecases/get_stored_email_delivery_state_interactor.dart +++ b/lib/features/push_notification/domain/usecases/get_stored_email_delivery_state_interactor.dart @@ -2,6 +2,8 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:fcm/model/type_name.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/get_stored_email_delivery_state.dart'; @@ -10,10 +12,10 @@ class GetStoredEmailDeliveryStateInteractor { GetStoredEmailDeliveryStateInteractor(this._fcmRepository); - Stream> execute() async* { + Stream> execute(AccountId accountId, UserName userName) async* { try { yield Right(GetStoredEmailDeliveryStateLoading()); - final storedState = await _fcmRepository.getStateToRefresh(TypeName.emailDelivery); + final storedState = await _fcmRepository.getStateToRefresh(accountId, userName, TypeName.emailDelivery); yield Right(GetStoredEmailDeliveryStateSuccess(storedState)); } catch (e) { yield Left(GetStoredEmailDeliveryStateFailure(e)); diff --git a/lib/features/push_notification/domain/usecases/store_email_delivery_state_interactor.dart b/lib/features/push_notification/domain/usecases/store_email_delivery_state_interactor.dart index 134c6a51a2..09cef827d9 100644 --- a/lib/features/push_notification/domain/usecases/store_email_delivery_state_interactor.dart +++ b/lib/features/push_notification/domain/usecases/store_email_delivery_state_interactor.dart @@ -2,7 +2,9 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:fcm/model/type_name.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/store_email_delivery_state.dart'; @@ -11,10 +13,10 @@ class StoreEmailDeliveryStateInteractor { StoreEmailDeliveryStateInteractor(this._fcmRepository); - Stream> execute(jmap.State newState) async* { + Stream> execute(AccountId accountId, UserName userName, jmap.State newState) async* { try { yield Right(StoreEmailDeliveryStateLoading()); - await _fcmRepository.storeStateToRefresh(TypeName.emailDelivery, newState); + await _fcmRepository.storeStateToRefresh(accountId, userName, TypeName.emailDelivery, newState); yield Right(StoreEmailDeliveryStateSuccess()); } catch (e) { yield Left(StoreEmailDeliveryStateFailure(e)); diff --git a/lib/features/push_notification/domain/usecases/store_email_state_to_refresh_interactor.dart b/lib/features/push_notification/domain/usecases/store_email_state_to_refresh_interactor.dart index 2f1398d0d4..3b229d93f2 100644 --- a/lib/features/push_notification/domain/usecases/store_email_state_to_refresh_interactor.dart +++ b/lib/features/push_notification/domain/usecases/store_email_state_to_refresh_interactor.dart @@ -2,7 +2,9 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:fcm/model/type_name.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/store_email_state_to_refresh_state.dart'; @@ -11,10 +13,10 @@ class StoreEmailStateToRefreshInteractor { StoreEmailStateToRefreshInteractor(this._fcmRepository); - Stream> execute(jmap.State newState) async* { + Stream> execute(AccountId accountId, UserName userName, jmap.State newState) async* { try { yield Right(StoreEmailStateToRefreshLoading()); - await _fcmRepository.storeStateToRefresh(TypeName.emailType, newState); + await _fcmRepository.storeStateToRefresh(accountId, userName, TypeName.emailType, newState); yield Right(StoreEmailStateToRefreshSuccess()); } catch (e) { yield Left(StoreEmailStateToRefreshFailure(e)); diff --git a/lib/features/push_notification/domain/usecases/store_mailbox_state_to_refresh_interactor.dart b/lib/features/push_notification/domain/usecases/store_mailbox_state_to_refresh_interactor.dart index 660db313a7..1f3382d165 100644 --- a/lib/features/push_notification/domain/usecases/store_mailbox_state_to_refresh_interactor.dart +++ b/lib/features/push_notification/domain/usecases/store_mailbox_state_to_refresh_interactor.dart @@ -2,7 +2,9 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:fcm/model/type_name.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_repository.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/store_mailbox_state_to_refresh_state.dart'; @@ -11,10 +13,10 @@ class StoreMailboxStateToRefreshInteractor { StoreMailboxStateToRefreshInteractor(this._fcmRepository); - Stream> execute(jmap.State newState) async* { + Stream> execute(AccountId accountId, UserName userName, jmap.State newState) async* { try { yield Right(StoreMailboxStateToRefreshLoading()); - await _fcmRepository.storeStateToRefresh(TypeName.mailboxType, newState); + await _fcmRepository.storeStateToRefresh(accountId, userName, TypeName.mailboxType, newState); yield Right(StoreMailboxStateToRefreshSuccess()); } catch (e) { yield Left(StoreMailboxStateToRefreshFailure(e)); diff --git a/lib/features/push_notification/domain/utils/fcm_constants.dart b/lib/features/push_notification/domain/utils/fcm_constants.dart new file mode 100644 index 0000000000..894bfb4303 --- /dev/null +++ b/lib/features/push_notification/domain/utils/fcm_constants.dart @@ -0,0 +1,14 @@ + +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; + +class FcmConstants { + + static final List mailboxRuleAllowPushNotifications = [ + PresentationMailbox.roleDrafts, + PresentationMailbox.roleSent, + PresentationMailbox.roleOutbox, + PresentationMailbox.roleSpam, + PresentationMailbox.roleTrash + ]; +} \ No newline at end of file diff --git a/lib/features/push_notification/presentation/action/fcm_action.dart b/lib/features/push_notification/presentation/action/fcm_action.dart index 9f82053347..f04eab4b50 100644 --- a/lib/features/push_notification/presentation/action/fcm_action.dart +++ b/lib/features/push_notification/presentation/action/fcm_action.dart @@ -1,49 +1,61 @@ import 'package:fcm/model/type_name.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; class SynchronizeEmailOnForegroundAction extends FcmStateChangeAction { final AccountId accountId; + final Session? session; SynchronizeEmailOnForegroundAction( TypeName typeName, jmap.State newState, - this.accountId + this.accountId, + this.session ) : super(typeName, newState); @override - List get props => [typeName, newState, accountId]; + List get props => [typeName, newState, accountId, session]; } class PushNotificationAction extends FcmStateChangeAction { + final Session? session; final AccountId accountId; + final UserName userName; PushNotificationAction( TypeName typeName, jmap.State newState, - this.accountId + this.session, + this.accountId, + this.userName ) : super(typeName, newState); @override - List get props => [typeName, newState, accountId]; + List get props => [typeName, newState, accountId, session, userName]; } class StoreEmailStateToRefreshAction extends FcmStateChangeAction { final AccountId accountId; + final UserName userName; + final Session? session; StoreEmailStateToRefreshAction( TypeName typeName, jmap.State newState, - this.accountId + this.accountId, + this.userName, + this.session ) : super(typeName, newState); @override - List get props => [typeName, newState, accountId]; + List get props => [typeName, newState, accountId, session]; } class SynchronizeMailboxOnForegroundAction extends FcmStateChangeAction { @@ -63,13 +75,15 @@ class SynchronizeMailboxOnForegroundAction extends FcmStateChangeAction { class StoreMailboxStateToRefreshAction extends FcmStateChangeAction { final AccountId accountId; + final UserName userName; StoreMailboxStateToRefreshAction( TypeName typeName, jmap.State newState, - this.accountId + this.accountId, + this.userName ) : super(typeName, newState); @override - List get props => [typeName, newState, accountId]; + List get props => [typeName, newState, accountId, userName]; } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart b/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart index a131bd6ccb..649e9e660f 100644 --- a/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart +++ b/lib/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart @@ -1,6 +1,32 @@ import 'package:core/data/model/source_type/data_source_type.dart'; +import 'package:core/utils/file_utils.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/datasource/html_datasource.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/html_datasource_impl.dart'; +import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; +import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; +import 'package:tmail_ui_user/features/email/data/repository/email_repository_impl.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/get_list_detailed_email_by_id_interator.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/store_list_new_email_interator.dart'; +import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; +import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; +import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart'; +import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/mailbox_datasource_impl.dart'; +import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/state_datasource_impl.dart'; +import 'package:tmail_ui_user/features/mailbox/data/local/mailbox_cache_manager.dart'; +import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_api.dart'; +import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_isolate_worker.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart'; import 'package:tmail_ui_user/features/push_notification/data/datasource/fcm_datasource.dart'; import 'package:tmail_ui_user/features/push_notification/data/datasource_impl/fcm_datasource_impl.dart'; import 'package:tmail_ui_user/features/push_notification/data/datasource_impl/cache_fcm_datasource_impl.dart'; @@ -11,10 +37,13 @@ import 'package:tmail_ui_user/features/push_notification/domain/repository/fcm_r import 'package:tmail_ui_user/features/push_notification/domain/usecases/delete_email_state_to_refresh_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/destroy_subscription_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_email_changes_to_push_notification_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_email_changes_to_remove_notification_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_email_state_to_refresh_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_fcm_subscription_local_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_firebase_subscription_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_mailbox_state_to_refresh_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_mailboxes_not_put_notifications_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_new_receive_email_from_notification_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_stored_email_delivery_state_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/register_new_token_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_email_delivery_state_interactor.dart'; @@ -23,6 +52,7 @@ import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_m import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_subscription_interator.dart'; import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart'; import 'package:tmail_ui_user/features/thread/data/datasource_impl/thread_datasource_impl.dart'; +import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_isolate_worker.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; @@ -34,6 +64,10 @@ class FcmInteractorBindings extends InteractorsBindings { void bindingsDataSource() { Get.lazyPut(() => Get.find()); Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); } @override @@ -51,6 +85,31 @@ class FcmInteractorBindings extends InteractorsBindings { Get.find(), Get.find() )); + Get.lazyPut(() => MailboxDataSourceImpl( + Get.find(), + Get.find(), + Get.find())); + Get.lazyPut(() => MailboxCacheDataSourceImpl( + Get.find(), + Get.find())); + Get.lazyPut(() => EmailDataSourceImpl( + Get.find(), + Get.find())); + Get.lazyPut(() => HtmlDataSourceImpl( + Get.find(), + Get.find())); + Get.lazyPut(() => StateDataSourceImpl( + Get.find(), + Get.find())); + Get.lazyPut(() => EmailHiveCacheDataSourceImpl( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find())); } @override @@ -58,6 +117,9 @@ class FcmInteractorBindings extends InteractorsBindings { Get.lazyPut(() => GetStoredEmailDeliveryStateInteractor(Get.find())); Get.lazyPut(() => StoreEmailDeliveryStateInteractor(Get.find())); Get.lazyPut(() => GetEmailChangesToPushNotificationInteractor(Get.find())); + Get.lazyPut(() => GetEmailChangesToRemoveNotificationInteractor( + Get.find(), + Get.find())); Get.lazyPut(() => StoreEmailStateToRefreshInteractor(Get.find())); Get.lazyPut(() => GetEmailStateToRefreshInteractor(Get.find())); Get.lazyPut(() => DeleteEmailStateToRefreshInteractor(Get.find())); @@ -68,11 +130,18 @@ class FcmInteractorBindings extends InteractorsBindings { Get.lazyPut(() => StoreSubscriptionInteractor(Get.find())); Get.lazyPut(() => GetFCMSubscriptionLocalInteractor(Get.find())); Get.lazyPut(() => DestroySubscriptionInteractor(Get.find())); + Get.lazyPut(() => GetMailboxesNotPutNotificationsInteractor(Get.find())); + Get.lazyPut(() => GetNewReceiveEmailFromNotificationInteractor( + Get.find(), + Get.find())); + Get.lazyPut(() => GetListDetailedEmailByIdInteractor(Get.find())); + Get.lazyPut(() => StoreListNewEmailInteractor(Get.find())); } @override void bindingsRepository() { Get.lazyPut(() => Get.find()); + Get.lazyPut(() => Get.find()); } @override @@ -82,7 +151,18 @@ class FcmInteractorBindings extends InteractorsBindings { DataSourceType.local: Get.find(), DataSourceType.network: Get.find(), }, - Get.find() + Get.find(), + { + DataSourceType.local: Get.find(), + DataSourceType.network: Get.find(), + }, )); + Get.lazyPut(() => EmailRepositoryImpl( + { + DataSourceType.network: Get.find(), + DataSourceType.hiveCache: Get.find() + }, + Get.find(), + Get.find())); } } diff --git a/lib/features/push_notification/presentation/config/fcm_configuration.dart b/lib/features/push_notification/presentation/config/fcm_configuration.dart index fa5ecb8638..77a9a72173 100644 --- a/lib/features/push_notification/presentation/config/fcm_configuration.dart +++ b/lib/features/push_notification/presentation/config/fcm_configuration.dart @@ -15,6 +15,7 @@ class FcmConfiguration { static void _initMessageListener() { FcmReceiver.instance.onForegroundMessage(); FcmReceiver.instance.onBackgroundMessage(); + FcmReceiver.instance.onMessageOpenedApp(); FcmReceiver.instance.getFcmToken(); FcmReceiver.instance.onRefreshFcmToken(); } diff --git a/lib/features/push_notification/presentation/controller/fcm_base_controller.dart b/lib/features/push_notification/presentation/controller/fcm_base_controller.dart new file mode 100644 index 0000000000..f3c9358a25 --- /dev/null +++ b/lib/features/push_notification/presentation/controller/fcm_base_controller.dart @@ -0,0 +1,25 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; + +abstract class FcmBaseController { + + void consumeState(Stream> newStateStream) { + newStateStream.listen( + _handleStateStream, + onError: (error, stackTrace) { + logError('FcmBaseController::consumeState():onError:error: $error | stackTrace: $stackTrace'); + } + ); + } + + void _handleStateStream(Either newState) { + newState.fold(handleFailureViewState, handleSuccessViewState); + } + + void handleFailureViewState(Failure failure); + + void handleSuccessViewState(Success success); +} \ No newline at end of file diff --git a/lib/features/push_notification/presentation/controller/fcm_controller.dart b/lib/features/push_notification/presentation/controller/fcm_message_controller.dart similarity index 59% rename from lib/features/push_notification/presentation/controller/fcm_controller.dart rename to lib/features/push_notification/presentation/controller/fcm_message_controller.dart index da59c1d32b..f1db98bcf4 100644 --- a/lib/features/push_notification/presentation/controller/fcm_controller.dart +++ b/lib/features/push_notification/presentation/controller/fcm_message_controller.dart @@ -6,16 +6,16 @@ import 'package:core/data/network/config/dynamic_url_interceptors.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:dartz/dartz.dart'; import 'package:fcm/model/type_name.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/push/state_change.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:rxdart/rxdart.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; -import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; import 'package:tmail_ui_user/features/home/presentation/home_bindings.dart'; import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; @@ -24,39 +24,52 @@ import 'package:tmail_ui_user/features/login/domain/state/get_credential_state.d import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_state.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/action/fcm_action.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/bindings/fcm_interactor_bindings.dart'; -import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_token_handler.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_base_controller.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_token_controller.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/extensions/state_change_extension.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/listener/email_change_listener.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/listener/mailbox_change_listener.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_service.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/utils/fcm_utils.dart'; +import 'package:tmail_ui_user/features/session/domain/extensions/session_extensions.dart'; +import 'package:tmail_ui_user/features/session/domain/state/get_session_state.dart'; +import 'package:tmail_ui_user/features/session/domain/usecases/get_session_interactor.dart'; import 'package:tmail_ui_user/main/bindings/main_bindings.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; -class FcmController extends BaseController { +class FcmMessageController extends FcmBaseController { AccountId? _currentAccountId; + Session? _currentSession; + UserName? _userName; RemoteMessage? _remoteMessageBackground; GetAuthenticatedAccountInteractor? _getAuthenticatedAccountInteractor; DynamicUrlInterceptors? _dynamicUrlInterceptors; AuthorizationInterceptors? _authorizationInterceptors; + GetSessionInteractor? _getSessionInteractor; + NewEmailCacheManager? _newEmailCacheManager; - FcmController._internal() { + FcmMessageController._internal() { _listenFcmStream(); } - static final FcmController _instance = FcmController._internal(); + static final FcmMessageController _instance = FcmMessageController._internal(); - static FcmController get instance => _instance; + static FcmMessageController get instance => _instance; - void initialize({AccountId? accountId}) { + void initializeFromAccountId(AccountId accountId, Session session) { _currentAccountId = accountId; - FcmTokenHandler.instance.initialize(); + _currentSession = session; + _userName = session.username; + FcmTokenController.instance.initialize(); } + void initialize() {} + void _listenFcmStream() async { await Future.wait([ listenForegroundMessageStream(), @@ -82,21 +95,21 @@ class FcmController extends BaseController { Future listenTokenStream() { FcmService.instance.fcmTokenStream .debounceTime(const Duration(milliseconds: FcmService.durationRefreshToken)) - .listen(FcmTokenHandler.instance.handleTokenAction); + .listen(FcmTokenController.instance.handleTokenAction); return Future.value(); } void _handleForegroundMessageAction(RemoteMessage newRemoteMessage) { - log('FcmController::_handleForegroundMessageAction():remoteMessage: ${newRemoteMessage.data}'); - if (_currentAccountId != null) { + log('FcmMessageController::_handleForegroundMessageAction():remoteMessage: ${newRemoteMessage.data} | _currentAccountId: $_currentAccountId'); + if (_currentAccountId != null && _userName != null) { final stateChange = _convertRemoteMessageToStateChange(newRemoteMessage); final mapTypeState = stateChange.getMapTypeState(_currentAccountId!); - _mappingTypeStateToAction(mapTypeState, _currentAccountId!); + _mappingTypeStateToAction(mapTypeState, _currentAccountId!, _userName!, session: _currentSession); } } void _handleBackgroundMessageAction(RemoteMessage newRemoteMessage) async { - log('FcmController::_handleBackgroundMessageAction():remoteMessage: ${newRemoteMessage.data}'); + log('FcmMessageController::_handleBackgroundMessageAction():remoteMessage: ${newRemoteMessage.data}'); _remoteMessageBackground = newRemoteMessage; await _initialAppConfig(); _getAuthenticatedAccount(); @@ -108,21 +121,23 @@ class FcmController extends BaseController { void _mappingTypeStateToAction( Map mapTypeState, - AccountId accountId, { + AccountId accountId, + UserName userName, { bool isForeground = true, + Session? session }) { - log('FcmController::_mappingTypeStateToAction():mapTypeState: $mapTypeState'); + log('FcmMessageController::_mappingTypeStateToAction():mapTypeState: $mapTypeState'); final listTypeName = mapTypeState.keys .map((value) => TypeName(value)) .toList(); final listEmailActions = listTypeName .where((typeName) => typeName == TypeName.emailType || typeName == TypeName.emailDelivery) - .map((typeName) => toFcmAction(typeName, accountId, mapTypeState, isForeground)) + .map((typeName) => toFcmAction(typeName, accountId, userName, mapTypeState, isForeground, session: session)) .whereNotNull() .toList(); - log('FcmController::_mappingTypeStateToAction():listEmailActions: $listEmailActions'); + log('FcmMessageController::_mappingTypeStateToAction():listEmailActions: $listEmailActions'); if (listEmailActions.isNotEmpty) { EmailChangeListener.instance.dispatchActions(listEmailActions); @@ -130,11 +145,11 @@ class FcmController extends BaseController { final listMailboxActions = listTypeName .where((typeName) => typeName == TypeName.mailboxType) - .map((typeName) => toFcmAction(typeName, accountId, mapTypeState, isForeground)) + .map((typeName) => toFcmAction(typeName, accountId, userName, mapTypeState, isForeground)) .whereNotNull() .toList(); - log('FcmController::_mappingTypeStateToAction():listMailboxActions: $listEmailActions'); + log('FcmMessageController::_mappingTypeStateToAction():listMailboxActions: $listEmailActions'); if (listMailboxActions.isNotEmpty) { MailboxChangeListener.instance.dispatchActions(listMailboxActions); @@ -144,25 +159,29 @@ class FcmController extends BaseController { FcmAction? toFcmAction( TypeName typeName, AccountId accountId, + UserName userName, Map mapTypeState, - isForeground + isForeground, + { + Session? session + } ) { final newState = jmap.State(mapTypeState[typeName.value]); if (typeName == TypeName.emailType) { if (isForeground) { - return SynchronizeEmailOnForegroundAction(typeName, newState, accountId); + return SynchronizeEmailOnForegroundAction(typeName, newState, accountId, session); } else { - return StoreEmailStateToRefreshAction(typeName, newState, accountId); + return StoreEmailStateToRefreshAction(typeName, newState, accountId, userName, session); } } else if (typeName == TypeName.emailDelivery) { if (!isForeground) { - return PushNotificationAction(typeName, newState, accountId); + return PushNotificationAction(typeName, newState, session, accountId, userName); } } else if (typeName == TypeName.mailboxType) { if (isForeground) { return SynchronizeMailboxOnForegroundAction(typeName, newState, accountId); } else { - return StoreMailboxStateToRefreshAction(typeName, newState, accountId); + return StoreMailboxStateToRefreshAction(typeName, newState, accountId, userName); } } return null; @@ -180,7 +199,9 @@ class FcmController extends BaseController { FcmInteractorBindings().dependencies(); }); - return await _getInteractorBindings(); + await _getInteractorBindings(); + + await _newEmailCacheManager?.closeNewEmailHiveCacheBox(); } Future _getInteractorBindings() { @@ -188,9 +209,11 @@ class FcmController extends BaseController { _getAuthenticatedAccountInteractor = getBinding(); _dynamicUrlInterceptors = getBinding(); _authorizationInterceptors = getBinding(); - FcmTokenHandler.instance.initialize(); + _getSessionInteractor = getBinding(); + _newEmailCacheManager = getBinding(); + FcmTokenController.instance.initialize(); } catch (e) { - logError('FcmController::_getBindings(): ${e.toString()}'); + logError('FcmMessageController::_getBindings(): ${e.toString()}'); } return Future.value(null); } @@ -200,56 +223,79 @@ class FcmController extends BaseController { consumeState(_getAuthenticatedAccountInteractor!.execute()); } else { _clearRemoteMessageBackground(); - logError('FcmController::_getAuthenticatedAccount():_getAuthenticatedAccountInteractor is null'); - } - } - - void _handleFailureViewState(Failure failure) { - log('FcmController::_handleFailureViewState(): $failure'); - _clearRemoteMessageBackground(); - } - - void _handleSuccessViewState(Success success) { - log('FcmController::_handleSuccessViewState(): $success'); - if (success is GetAuthenticatedAccountSuccess) { - _handleGetAuthenticatedAccountSuccess(success); - } else if (success is GetStoredTokenOidcSuccess) { - _handleGetAccountByOidcSuccess(success); - } else if (success is GetCredentialViewState) { - _handleGetAccountByBasicAuthSuccess(success); + logError('FcmMessageController::_getAuthenticatedAccount():_getAuthenticatedAccountInteractor is null'); } } void _handleGetAuthenticatedAccountSuccess(GetAuthenticatedAccountSuccess success) { _currentAccountId = success.account.accountId; - _dynamicUrlInterceptors?.changeBaseUrl(success.account.apiUrl); - log('FcmController::_handleGetAuthenticatedAccountSuccess():_currentAccountId: $_currentAccountId'); + _userName = success.account.userName; + if (!FcmUtils.instance.isMobileAndroid) { + _dynamicUrlInterceptors?.changeBaseUrl(success.account.apiUrl); + } + log('FcmMessageController::_handleGetAuthenticatedAccountSuccess():_currentAccountId: $_currentAccountId | _userName: $_userName'); } void _handleGetAccountByOidcSuccess(GetStoredTokenOidcSuccess storedTokenOidcSuccess) { - log('FcmController::_handleGetAccountByOidcSuccess():'); + log('FcmMessageController::_handleGetAccountByOidcSuccess():'); + _dynamicUrlInterceptors?.setJmapUrl(storedTokenOidcSuccess.baseUrl.toString()); _authorizationInterceptors?.setTokenAndAuthorityOidc( newToken: storedTokenOidcSuccess.tokenOidc.toToken(), newConfig: storedTokenOidcSuccess.oidcConfiguration ); - _pushActionFromRemoteMessageBackground(); + + if (FcmUtils.instance.isMobileAndroid) { + _dynamicUrlInterceptors?.changeBaseUrl(storedTokenOidcSuccess.baseUrl.toString()); + _getSessionAction(); + } else { + _pushActionFromRemoteMessageBackground(); + } } void _handleGetAccountByBasicAuthSuccess(GetCredentialViewState credentialViewState) { - log('FcmController::_handleGetAccountByBasicAuthSuccess():'); + log('FcmMessageController::_handleGetAccountByBasicAuthSuccess():'); + _dynamicUrlInterceptors?.setJmapUrl(credentialViewState.baseUrl.toString()); _authorizationInterceptors?.setBasicAuthorization( - credentialViewState.userName.userName, + credentialViewState.userName.value, credentialViewState.password.value, ); - _pushActionFromRemoteMessageBackground(); + if (FcmUtils.instance.isMobileAndroid) { + _dynamicUrlInterceptors?.changeBaseUrl(credentialViewState.baseUrl.toString()); + _getSessionAction(); + } else { + _pushActionFromRemoteMessageBackground(); + } + } + + void _getSessionAction() { + if (_getSessionInteractor != null) { + consumeState(_getSessionInteractor!.execute()); + } else { + _clearRemoteMessageBackground(); + logError('FcmMessageController::_getSessionAction():_getSessionInteractor is null'); + } + } + + void _handleGetSessionSuccess(GetSessionSuccess success) { + _currentSession = success.session; + _userName = success.session.username; + final apiUrl = success.session.getQualifiedApiUrl(baseUrl: _dynamicUrlInterceptors?.jmapUrl); + log('FcmMessageController::_pushActionFromRemoteMessageBackground():apiUrl: $apiUrl'); + if (apiUrl.isNotEmpty) { + _dynamicUrlInterceptors?.changeBaseUrl(apiUrl); + _pushActionFromRemoteMessageBackground(); + } else { + _clearRemoteMessageBackground(); + logError('FcmMessageController::_handleGetSessionSuccess():apiUrl is null'); + } } void _pushActionFromRemoteMessageBackground() { - log('FcmController::_pushActionFromRemoteMessageBackground():'); - if (_remoteMessageBackground != null && _currentAccountId != null) { + log('FcmMessageController::_pushActionFromRemoteMessageBackground():_remoteMessageBackground: $_remoteMessageBackground | _currentAccountId: $_currentAccountId | _currentSession: $_currentSession'); + if (_remoteMessageBackground != null && _currentAccountId != null && _userName != null) { final stateChange = _convertRemoteMessageToStateChange(_remoteMessageBackground!); final mapTypeState = stateChange.getMapTypeState(_currentAccountId!); - _mappingTypeStateToAction(mapTypeState, _currentAccountId!, isForeground: false); + _mappingTypeStateToAction(mapTypeState, _currentAccountId!, _userName!, isForeground: false, session: _currentSession); } _clearRemoteMessageBackground(); } @@ -259,11 +305,22 @@ class FcmController extends BaseController { } @override - void onData(Either newState) { - super.onData(newState); - newState.fold(_handleFailureViewState, _handleSuccessViewState); + void handleFailureViewState(Failure failure) { + log('FcmMessageController::_handleFailureViewState(): $failure'); + _clearRemoteMessageBackground(); } @override - void onDone() {} + void handleSuccessViewState(Success success) { + log('FcmMessageController::_handleSuccessViewState(): $success'); + if (success is GetAuthenticatedAccountSuccess) { + _handleGetAuthenticatedAccountSuccess(success); + } else if (success is GetSessionSuccess) { + _handleGetSessionSuccess(success); + } else if (success is GetStoredTokenOidcSuccess) { + _handleGetAccountByOidcSuccess(success); + } else if (success is GetCredentialViewState) { + _handleGetAccountByBasicAuthSuccess(success); + } + } } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/controller/fcm_token_handler.dart b/lib/features/push_notification/presentation/controller/fcm_token_controller.dart similarity index 72% rename from lib/features/push_notification/presentation/controller/fcm_token_handler.dart rename to lib/features/push_notification/presentation/controller/fcm_token_controller.dart index 2822ae1d68..6ee1899e52 100644 --- a/lib/features/push_notification/presentation/controller/fcm_token_handler.dart +++ b/lib/features/push_notification/presentation/controller/fcm_token_controller.dart @@ -3,7 +3,6 @@ import 'package:core/domain/extensions/datetime_extension.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:dartz/dartz.dart'; import 'package:fcm/model/device_client_id.dart'; import 'package:fcm/model/firebase_expired_time.dart'; import 'package:fcm/model/firebase_subscription.dart'; @@ -20,17 +19,18 @@ import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_fcm import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_firebase_subscription_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/register_new_token_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_subscription_interator.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_base_controller.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/utils/fcm_utils.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:uuid/uuid.dart'; -class FcmTokenHandler { +class FcmTokenController extends FcmBaseController { - FcmTokenHandler._internal(); + FcmTokenController._internal(); - static final FcmTokenHandler _instance = FcmTokenHandler._internal(); + static final FcmTokenController _instance = FcmTokenController._internal(); - static FcmTokenHandler get instance => _instance; + static FcmTokenController get instance => _instance; static const int limitedTimeToExpire = 3; static const int extensionTimeExpire = 7; @@ -50,18 +50,18 @@ class FcmTokenHandler { _registerNewTokenInteractor = getBinding(); _getFCMSubscriptionLocalInteractor = getBinding(); } catch (e) { - logError('FcmTokenHandler::initialize(): ${e.toString()}'); + logError('FcmTokenController::initialize(): ${e.toString()}'); } } void handleTokenAction(String? token) { - log('FcmTokenHandler::handleTokenAction():token: $token'); + log('FcmTokenController::handleTokenAction():token: $token'); if (token != null) { _fcmToken = FirebaseToken(token); final deviceId = FcmUtils.instance.hashTokenToDeviceId(token); _deviceClientId = DeviceClientId(deviceId); - log('FcmTokenHandler::handleTokenAction(): fcmToken: $_fcmToken'); - log('FcmTokenHandler::handleTokenAction(): deviceId: $deviceId'); + log('FcmTokenController::handleTokenAction(): fcmToken: $_fcmToken'); + log('FcmTokenController::handleTokenAction(): deviceId: $deviceId'); _getFcmTokenFromBackend(deviceId); } else { _getFCMSubscriptionLocalAction(); @@ -70,68 +70,24 @@ class FcmTokenHandler { void _getFcmTokenFromBackend(String deviceId) { if (_getFirebaseSubscriptionInteractor != null) { - _consumeState(_getFirebaseSubscriptionInteractor!.execute(deviceId)); + consumeState(_getFirebaseSubscriptionInteractor!.execute(deviceId)); } } void _storeSubscriptionAction(FCMSubscription fcmSubscription){ if (_storeSubscriptionInteractor != null) { - _consumeState(_storeSubscriptionInteractor!.execute(fcmSubscription)); - } - } - - void _consumeState(Stream> newStateStream) { - newStateStream.listen( - _handleStateStream, - onError: (error, stackTrace) { - logError('FcmTokenHandler::consumeState():onError:error: $error'); - logError('FcmTokenHandler::consumeState():onError:stackTrace: $stackTrace'); - } - ); - } - - void _handleStateStream(Either newState) { - newState.fold(_handleFailureViewState, _handleSuccessViewState); - } - - void _handleFailureViewState(Failure failure) { - log('FcmTokenHandler::_handleFailureViewState(): $failure'); - if (failure is GetFirebaseSubscriptionFailure) { - if (_fcmToken != null && _deviceClientId != null) { - _handleRegisterNewToken(_fcmToken!, _deviceClientId!); - } - } - } - - void _handleSuccessViewState(Success success) { - log('FcmTokenHandler::_handleSuccessViewState(): $success'); - if (success is GetFirebaseSubscriptionSuccess) { - _deviceClientId = success.firebaseSubscription.deviceClientId; - final expireTime = success.firebaseSubscription.expires; - log('FcmTokenHandler::_handleSuccessViewState():_fcmToken: $_fcmToken'); - if (_isTokenExpired(expireTime)) { - log('FcmTokenHandler::_handleSuccessViewState(): _isTokenExpired true'); - _handleWhenTokenExpired(); - } - } else if (success is RegisterNewTokenSuccess) { - final deviceId = success.firebaseSubscription.deviceClientId?.value; - final subscriptionId = success.firebaseSubscription.id?.id.value; - if (deviceId != null && subscriptionId != null) { - _storeSubscriptionAction(FCMSubscription(deviceId, subscriptionId)); - } - } else if (success is GetFCMSubscriptionLocalSuccess) { - _getFcmTokenFromBackend(success.fcmSubscription.deviceId); + consumeState(_storeSubscriptionInteractor!.execute(fcmSubscription)); } } bool _isTokenExpired(FirebaseExpiredTime? expireTime) { - log('FcmTokenHandler::_isTokenExpired():expireTime: $expireTime'); + log('FcmTokenController::_isTokenExpired():expireTime: $expireTime'); if (expireTime != null) { final expireTimeLocal = expireTime.value.value.toLocal(); final currentTime = DateTime.now(); - log('FcmTokenHandler::_isTokenExpired():expireTimeLocal: $expireTimeLocal'); - log('FcmTokenHandler::_isTokenExpired():currentTime: $currentTime'); + log('FcmTokenController::_isTokenExpired():expireTimeLocal: $expireTimeLocal'); + log('FcmTokenController::_isTokenExpired():currentTime: $currentTime'); return currentTime.isBefore(expireTimeLocal) && expireTimeLocal.daysBetween(currentTime) <= limitedTimeToExpire; @@ -142,14 +98,14 @@ class FcmTokenHandler { void _handleWhenTokenExpired() { if (_fcmToken == null || _deviceClientId == null) { - log('FcmTokenHandler::_handleSuccessViewState():_fcmToken or _deviceClientId is null'); + log('FcmTokenController::_handleSuccessViewState():_fcmToken or _deviceClientId is null'); return; } final generateCreationId = Id(const Uuid().v4()); final newExpireTime = DateTime.now().add(const Duration(days: extensionTimeExpire)); - log('FcmTokenHandler::_handleSuccessViewState():newExpireTime: $newExpireTime'); + log('FcmTokenController::_handleSuccessViewState():newExpireTime: $newExpireTime'); final firebaseSubscription = FirebaseSubscription( token: _fcmToken!, expires: FirebaseExpiredTime(newExpireTime.toUTCDate()!), @@ -157,7 +113,7 @@ class FcmTokenHandler { types: [TypeName.emailType, TypeName.mailboxType, TypeName.emailDelivery] ); - log('FcmTokenHandler::_handleSuccessViewState():firebaseSubscription: $firebaseSubscription'); + log('FcmTokenController::_handleSuccessViewState():firebaseSubscription: $firebaseSubscription'); _invokeRegisterNewTokenAction(RegisterNewTokenRequest( generateCreationId, firebaseSubscription @@ -171,7 +127,7 @@ class FcmTokenHandler { deviceClientId: deviceClientId, types: [TypeName.emailType, TypeName.mailboxType, TypeName.emailDelivery] ); - log('FcmTokenHandler::_handleRegisterNewToken():firebaseSubscription: $firebaseSubscription'); + log('FcmTokenController::_handleRegisterNewToken():firebaseSubscription: $firebaseSubscription'); _invokeRegisterNewTokenAction(RegisterNewTokenRequest( generateCreationId, firebaseSubscription @@ -180,13 +136,45 @@ class FcmTokenHandler { void _invokeRegisterNewTokenAction(RegisterNewTokenRequest newTokenRequest) { if (_registerNewTokenInteractor != null) { - _consumeState(_registerNewTokenInteractor!.execute(newTokenRequest)); + consumeState(_registerNewTokenInteractor!.execute(newTokenRequest)); } } void _getFCMSubscriptionLocalAction() { if (_getFCMSubscriptionLocalInteractor != null) { - _consumeState(_getFCMSubscriptionLocalInteractor!.execute()); + consumeState(_getFCMSubscriptionLocalInteractor!.execute()); + } + } + + @override + void handleFailureViewState(Failure failure) { + log('FcmTokenController::_handleFailureViewState(): $failure'); + if (failure is GetFirebaseSubscriptionFailure) { + if (_fcmToken != null && _deviceClientId != null) { + _handleRegisterNewToken(_fcmToken!, _deviceClientId!); + } + } + } + + @override + void handleSuccessViewState(Success success) { + log('FcmTokenController::_handleSuccessViewState(): $success'); + if (success is GetFirebaseSubscriptionSuccess) { + _deviceClientId = success.firebaseSubscription.deviceClientId; + final expireTime = success.firebaseSubscription.expires; + log('FcmTokenController::_handleSuccessViewState():_fcmToken: $_fcmToken'); + if (_isTokenExpired(expireTime)) { + log('FcmTokenController::_handleSuccessViewState(): _isTokenExpired true'); + _handleWhenTokenExpired(); + } + } else if (success is RegisterNewTokenSuccess) { + final deviceId = success.firebaseSubscription.deviceClientId?.value; + final subscriptionId = success.firebaseSubscription.id?.id.value; + if (deviceId != null && subscriptionId != null) { + _storeSubscriptionAction(FCMSubscription(deviceId, subscriptionId)); + } + } else if (success is GetFCMSubscriptionLocalSuccess) { + _getFcmTokenFromBackend(success.fcmSubscription.deviceId); } } } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/listener/email_change_listener.dart b/lib/features/push_notification/presentation/listener/email_change_listener.dart index 0ba5def291..7535b70d94 100644 --- a/lib/features/push_notification/presentation/listener/email_change_listener.dart +++ b/lib/features/push_notification/presentation/listener/email_change_listener.dart @@ -1,23 +1,46 @@ import 'dart:io'; +import 'package:core/data/network/config/dynamic_url_interceptors.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_comparator.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_comparator_property.dart'; +import 'package:model/email/email_property.dart'; import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/list_presentation_email_extension.dart'; +import 'package:model/extensions/list_presentation_mailbox_extension.dart'; +import 'package:model/extensions/session_extension.dart'; import 'package:model/notification/notification_payload.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; +import 'package:tmail_ui_user/features/email/domain/model/detailed_email.dart'; +import 'package:tmail_ui_user/features/email/domain/state/get_detailed_email_by_id_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/get_stored_state_email_state.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/get_list_detailed_email_by_id_interator.dart'; import 'package:tmail_ui_user/features/email/domain/usecases/get_stored_email_state_interactor.dart'; +import 'package:tmail_ui_user/features/email/domain/usecases/store_list_new_email_interator.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/push_notification/domain/exceptions/fcm_exception.dart'; -import 'package:tmail_ui_user/features/push_notification/domain/state/get_email_changes_state.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/get_email_changes_to_push_notification_state.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/get_email_changes_to_remove_notification_state.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/get_mailboxes_not_put_notifications_state.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/state/get_new_receive_email_from_notification_state.dart'; import 'package:tmail_ui_user/features/push_notification/domain/state/get_stored_email_delivery_state.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_email_changes_to_push_notification_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_email_changes_to_remove_notification_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_mailboxes_not_put_notifications_interactor.dart'; +import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_new_receive_email_from_notification_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/get_stored_email_delivery_state_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_email_delivery_state_interactor.dart'; import 'package:tmail_ui_user/features/push_notification/domain/usecases/store_email_state_to_refresh_interactor.dart'; @@ -38,9 +61,18 @@ class EmailChangeListener extends ChangeListener { GetEmailChangesToPushNotificationInteractor? _getEmailChangesToPushNotificationInteractor; GetStoredEmailStateInteractor? _getStoredEmailStateInteractor; StoreEmailStateToRefreshInteractor? _storeEmailStateToRefreshInteractor; + GetMailboxesNotPutNotificationsInteractor? _getMailboxesNotPutNotificationsInteractor; + GetEmailChangesToRemoveNotificationInteractor? _getEmailChangesToRemoveNotificationInteractor; + GetNewReceiveEmailFromNotificationInteractor? _getNewReceiveEmailFromNotificationInteractor; + GetListDetailedEmailByIdInteractor? _getListDetailedEmailByIdInteractor; + DynamicUrlInterceptors? _dynamicUrlInterceptors; + StoreListNewEmailInteractor? _storeListNewEmailInteractor; - jmap.State? _newState; + jmap.State? _newStateEmailDelivery; AccountId? _accountId; + Session? _session; + UserName? _userName; + List _emailsAvailablePushNotification = []; EmailChangeListener._internal() { try { @@ -50,6 +82,12 @@ class EmailChangeListener extends ChangeListener { _storeEmailDeliveryStateInteractor = getBinding(); _getEmailChangesToPushNotificationInteractor = getBinding(); _storeEmailStateToRefreshInteractor = getBinding(); + _getMailboxesNotPutNotificationsInteractor = getBinding(); + _getEmailChangesToRemoveNotificationInteractor = getBinding(); + _getNewReceiveEmailFromNotificationInteractor = getBinding(); + _getListDetailedEmailByIdInteractor = getBinding(); + _dynamicUrlInterceptors = getBinding(); + _storeListNewEmailInteractor = getBinding(); } catch (e) { logError('EmailChangeListener::_internal(): IS NOT REGISTERED: ${e.toString()}'); } @@ -64,11 +102,24 @@ class EmailChangeListener extends ChangeListener { log('EmailChangeListener::dispatchActions():actions: $actions'); for (var action in actions) { if (action is SynchronizeEmailOnForegroundAction) { + if (FcmUtils.instance.isMobileAndroid) { + _handleRemoveNotificationWhenEmailMarkAsRead(action.newState, action.accountId, action.session); + } _synchronizeEmailOnForegroundAction(action.newState); + if (PlatformInfo.isMobile) { + _getNewReceiveEmailFromNotificationAction(action.session, action.accountId, action.newState); + } } else if (action is PushNotificationAction) { - _pushNotificationAction(action.newState, action.accountId); + _pushNotificationAction(action.newState, action.accountId, action.userName, action.session); + + if (FcmUtils.instance.isMobileAndroid) { + _getNewReceiveEmailFromNotificationAction(action.session, action.accountId, action.newState); + } } else if (action is StoreEmailStateToRefreshAction) { - _handleStoreEmailStateToRefreshAction(action.newState); + if (FcmUtils.instance.isMobileAndroid) { + _handleRemoveNotificationWhenEmailMarkAsRead(action.newState, action.accountId, action.session); + } + _handleStoreEmailStateToRefreshAction(action.accountId, action.userName, action.newState); } } } @@ -80,55 +131,60 @@ class EmailChangeListener extends ChangeListener { } } - void _pushNotificationAction(jmap.State newState, AccountId accountId) { - _newState = newState; + void _pushNotificationAction(jmap.State newState, AccountId accountId, UserName userName, Session? session) { + _newStateEmailDelivery = newState; _accountId = accountId; + _session = session; + _userName = userName; log('EmailChangeListener::_pushNotificationAction():newState: $newState'); - if (BuildUtils.isWeb) { - _storeEmailDeliveryStateAction(_newState!); + if (PlatformInfo.isWeb) { + _storeEmailDeliveryStateAction(accountId, userName, _newStateEmailDelivery!); } else { if (Platform.isAndroid) { - _getStoredEmailDeliveryState(); + _getStoredEmailDeliveryState(accountId, userName); } else if (Platform.isIOS) { - _storeEmailDeliveryStateAction(_newState!); - _showLocalNotificationForIOS(_newState!, _accountId!); + _storeEmailDeliveryStateAction(accountId, userName, _newStateEmailDelivery!); + _showLocalNotificationForIOS(_newStateEmailDelivery!, accountId); } else { logError('EmailChangeListener::_pushNotificationAction(): NOT SUPPORTED PLATFORM'); } } } - - - void _getStoredEmailDeliveryState() { + void _getStoredEmailDeliveryState(AccountId accountId, UserName userName) { if (_getStoredEmailDeliveryStateInteractor != null) { - consumeState(_getStoredEmailDeliveryStateInteractor!.execute()); + consumeState(_getStoredEmailDeliveryStateInteractor!.execute(accountId, userName)); } } void _getStoredEmailState() { - if (_getStoredEmailStateInteractor != null && _accountId != null) { - consumeState(_getStoredEmailStateInteractor!.execute(_accountId!)); + if (_getStoredEmailStateInteractor != null && _session != null && _accountId != null) { + consumeState(_getStoredEmailStateInteractor!.execute(_session!, _accountId!)); } else { logError('EmailChangeListener::_getStoredEmailState(): _getStoredEmailStateInteractor is null'); } } void _getEmailChangesAction(jmap.State state) { - if (_getEmailChangesToPushNotificationInteractor != null && _accountId != null) { + if (_getEmailChangesToPushNotificationInteractor != null && + _accountId != null && + _session != null && + _userName != null) { consumeState(_getEmailChangesToPushNotificationInteractor!.execute( + _session!, _accountId!, + _userName!, state, - propertiesCreated: ThreadConstants.propertiesDefault, + propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, )); } } - void _storeEmailDeliveryStateAction(jmap.State state) { + void _storeEmailDeliveryStateAction(AccountId accountId, UserName userName, jmap.State state) { if (_storeEmailDeliveryStateInteractor != null) { - consumeState(_storeEmailDeliveryStateInteractor!.execute(state)); + consumeState(_storeEmailDeliveryStateInteractor!.execute(accountId, userName, state)); } } @@ -136,7 +192,7 @@ class EmailChangeListener extends ChangeListener { final notificationPayload = NotificationPayload(emailId: presentationEmail.id); log('EmailChangeListener::_showLocalNotification():notificationPayload: $notificationPayload'); LocalNotificationManager.instance.showPushNotification( - id: presentationEmail.id.id.value, + id: presentationEmail.id?.id.value ?? '', title: presentationEmail.subject ?? '', message: presentationEmail.preview, emailAddress: presentationEmail.from?.first, @@ -144,7 +200,7 @@ class EmailChangeListener extends ChangeListener { ); } - void _showLocalNotificationForIOS(jmap.State newState, AccountId accountId) { + void _showLocalNotificationForIOS(jmap.State newState, AccountId accountId) async { final notificationPayload = NotificationPayload(newState: newState); log('EmailChangeListener::_showLocalNotificationForIOS():notificationPayload: $notificationPayload'); LocalNotificationManager.instance.showPushNotification( @@ -157,6 +213,7 @@ class EmailChangeListener extends ChangeListener { : LocalNotificationConfig.notificationMessage, payload: notificationPayload.encodeToString, ); + await LocalNotificationManager.instance.setNotificationBadgeForIOS(); } @override @@ -165,6 +222,9 @@ class EmailChangeListener extends ChangeListener { if (failure is GetStoredEmailDeliveryStateFailure && failure.exception is NotFoundEmailDeliveryStateException) { _getStoredEmailState(); + } else if (failure is GetMailboxesNotPutNotificationsFailure) { + final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification(); + _handleLocalPushNotification(listEmails); } } @@ -172,36 +232,128 @@ class EmailChangeListener extends ChangeListener { void handleSuccessViewState(Success success) { log('EmailChangeListener::_handleSuccessViewState(): $success'); if (success is GetStoredEmailDeliveryStateSuccess) { - if (_newState != success.state) { + if (_newStateEmailDelivery != success.state) { _getEmailChangesAction(success.state); } } else if (success is GetStoredEmailStateSuccess) { _getEmailChangesAction(success.state); } else if (success is GetEmailChangesToPushNotificationSuccess) { - if (_newState != null) { - _storeEmailDeliveryStateAction(_newState!); + if (_newStateEmailDelivery != null) { + _storeEmailDeliveryStateAction(success.accountId, success.userName, _newStateEmailDelivery!); - if (!BuildUtils.isWeb && Platform.isAndroid) { - _handleLocalPushNotification(success.emailList); + if (FcmUtils.instance.isMobileAndroid) { + _handleListEmailToPushNotification(success.emailList); } } + } else if (success is GetMailboxesNotPutNotificationsSuccess) { + final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification( + mailboxIdsNotPutNotifications: success.mailboxes.mailboxIds); + _handleLocalPushNotification(listEmails); + } else if (success is GetEmailChangesToRemoveNotificationSuccess) { + _handleRemoveLocalNotification(success.emailIds); + } else if (success is GetNewReceiveEmailFromNotificationSuccess) { + _getListDetailedEmailByIdAction(success.session, success.accountId, success.emailIds); + } else if (success is GetDetailedEmailByIdSuccess) { + _storeNewEmailAction( + success.session, + success.accountId, + success.mapDetailedEmail); + } + } + + void _handleListEmailToPushNotification(List emailList) { + _emailsAvailablePushNotification = emailList; + if (_getMailboxesNotPutNotificationsInteractor != null && _accountId != null && _session != null) { + consumeState(_getMailboxesNotPutNotificationsInteractor!.execute(_session!, _accountId!)); + } else { + final listEmails = _emailsAvailablePushNotification.toEmailsAvailablePushNotification(); + _handleLocalPushNotification(listEmails); } } void _handleLocalPushNotification(List emailList) { + log('EmailChangeListener::_handleLocalPushNotification():emailList: $emailList'); + if (emailList.isEmpty) { + return; + } + for (var presentationEmail in emailList) { _showLocalNotification(presentationEmail); } LocalNotificationManager.instance.groupPushNotification(); + + _emailsAvailablePushNotification.clear(); } - void _handleStoreEmailStateToRefreshAction(jmap.State newState) { + void _handleStoreEmailStateToRefreshAction(AccountId accountId, UserName userName, jmap.State newState) { log('EmailChangeListener::_handleStoreEmailStateToRefreshAction():newState: $newState'); if (_storeEmailStateToRefreshInteractor != null) { - consumeState(_storeEmailStateToRefreshInteractor!.execute(newState)); + consumeState(_storeEmailStateToRefreshInteractor!.execute(accountId, userName, newState)); } else { logError('EmailChangeListener::_handleStoreEmailStateToRefreshAction():_storeEmailStateToRefreshInteractor is null'); } } + + void _handleRemoveNotificationWhenEmailMarkAsRead(jmap.State newState, AccountId accountId, Session? session) { + if (_getEmailChangesToRemoveNotificationInteractor != null && session != null) { + consumeState(_getEmailChangesToRemoveNotificationInteractor!.execute( + session, + accountId, + newState, + propertiesCreated: Properties({EmailProperty.id, EmailProperty.keywords}), + propertiesUpdated: Properties({EmailProperty.keywords}), + )); + } + } + + void _handleRemoveLocalNotification(List emailIds) async { + log('EmailChangeListener::_handleRemoveLocalNotification():emailIds: $emailIds'); + await Future.wait(emailIds.map((emailId) => LocalNotificationManager.instance.removeNotification(emailId.id.value))); + LocalNotificationManager.instance.removeGroupPushNotification(); + } + + void _getNewReceiveEmailFromNotificationAction(Session? session, AccountId accountId, jmap.State newState) { + if (_getNewReceiveEmailFromNotificationInteractor != null && session != null) { + consumeState(_getNewReceiveEmailFromNotificationInteractor!.execute( + session: session, + accountId: accountId, + userName: session.username, + newState: newState + )); + } + } + + void _getListDetailedEmailByIdAction(Session? session, AccountId accountId, Set emailIds) { + log('EmailChangeListener::_getListDetailedEmailByIdAction():emailIds: $emailIds'); + if (_getListDetailedEmailByIdInteractor != null && + _dynamicUrlInterceptors != null && + session != null) { + final baseDownloadUrl = session.getDownloadUrl(jmapUrl: _dynamicUrlInterceptors!.jmapUrl); + consumeState(_getListDetailedEmailByIdInteractor!.execute( + session, + accountId, + emailIds, + baseDownloadUrl, + sort: {} + ..add(EmailComparator(EmailComparatorProperty.receivedAt) + ..setIsAscending(true)) + )); + } + } + + void _storeNewEmailAction( + Session session, + AccountId accountId, + Map mapDetailedEmails + ) { + log('EmailChangeListener::_storeNewEmailAction():mapDetailedEmails: ${mapDetailedEmails.length}'); + if (_storeListNewEmailInteractor != null) { + consumeState(_storeListNewEmailInteractor!.execute( + session, + accountId, + mapDetailedEmails + )); + } + } } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/listener/mailbox_change_listener.dart b/lib/features/push_notification/presentation/listener/mailbox_change_listener.dart index b0339906d9..1ff676f91c 100644 --- a/lib/features/push_notification/presentation/listener/mailbox_change_listener.dart +++ b/lib/features/push_notification/presentation/listener/mailbox_change_listener.dart @@ -2,7 +2,9 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:tmail_ui_user/features/base/action/ui_action.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; @@ -36,7 +38,7 @@ class MailboxChangeListener extends ChangeListener { if (action is SynchronizeMailboxOnForegroundAction) { _synchronizeMailboxOnForegroundAction(action.newState); } else if (action is StoreMailboxStateToRefreshAction) { - _handleStoreMailboxStateToRefreshAction(action.newState); + _handleStoreMailboxStateToRefreshAction(action.accountId, action.userName, action.newState); } } } @@ -58,10 +60,10 @@ class MailboxChangeListener extends ChangeListener { } } - void _handleStoreMailboxStateToRefreshAction(jmap.State newState) { + void _handleStoreMailboxStateToRefreshAction(AccountId accountId, UserName userName, jmap.State newState) { log('MailboxChangeListener::_handleStoreMailboxStateToRefreshAction():newState: $newState'); if (_storeMailboxStateToRefreshInteractor != null) { - consumeState(_storeMailboxStateToRefreshInteractor!.execute(newState)); + consumeState(_storeMailboxStateToRefreshInteractor!.execute(accountId, userName, newState)); } } } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/notification/local_notification_config.dart b/lib/features/push_notification/presentation/notification/local_notification_config.dart index 975979bf15..8af71cd807 100644 --- a/lib/features/push_notification/presentation/notification/local_notification_config.dart +++ b/lib/features/push_notification/presentation/notification/local_notification_config.dart @@ -9,6 +9,8 @@ class LocalNotificationConfig { static const _channelDescription = 'Team Mail notifications'; static const notificationTitle = 'Team Mail'; static const notificationMessage = 'You have new messages'; + static const messageHasBeenSentSuccessfully = 'Message has been sent successfully.'; + static const int groupNotificationId = 1995; static const iosInitializationSettings = DarwinInitializationSettings(); diff --git a/lib/features/push_notification/presentation/notification/local_notification_manager.dart b/lib/features/push_notification/presentation/notification/local_notification_manager.dart index 3f187d066f..abf2f2c2ed 100644 --- a/lib/features/push_notification/presentation/notification/local_notification_manager.dart +++ b/lib/features/push_notification/presentation/notification/local_notification_manager.dart @@ -4,10 +4,13 @@ import 'dart:io'; import 'package:core/presentation/extensions/html_extension.dart'; import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter_app_badger/flutter_app_badger.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:model/extensions/email_address_extension.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_config.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/utils/fcm_utils.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -88,6 +91,8 @@ class LocalNotificationManager { final granted = await _isAndroidPermissionGranted(); if (!granted) { _notificationsEnabled = await _requestPermissions(); + } else { + _notificationsEnabled = granted; } } else { _notificationsEnabled = await _requestPermissions(); @@ -129,24 +134,42 @@ class LocalNotificationManager { required String title, String? message, EmailAddress? emailAddress, - String? payload + String? payload, + bool isInboxStyle = true }) async { - final inboxStyleInformation = InboxStyleInformation( - [message?.addBlockTag('p', attribute: 'style="color:#6D7885;"') ?? ''], - htmlFormatLines: true, - contentTitle: title, - htmlFormatContentTitle: true, - summaryText: (emailAddress?.asString() ?? '').addBlockTag('b'), - htmlFormatSummaryText: true, - ); + if (isInboxStyle) { + final inboxStyleInformation = InboxStyleInformation( + [message?.addBlockTag('p', attribute: 'style="color:#6D7885;"') ?? ''], + htmlFormatLines: true, + contentTitle: title, + htmlFormatContentTitle: true, + summaryText: (emailAddress?.asString() ?? '').addBlockTag('b'), + htmlFormatSummaryText: true, + ); - await _localNotificationsPlugin.show( - id.hashCode, - title, - message, - LocalNotificationConfig.instance.generateNotificationDetails(styleInformation: inboxStyleInformation), - payload: payload - ); + await _localNotificationsPlugin.show( + id.hashCode, + title, + message, + LocalNotificationConfig.instance.generateNotificationDetails(styleInformation: inboxStyleInformation), + payload: payload + ); + } else { + await _localNotificationsPlugin.show( + id.hashCode, + title, + message, + LocalNotificationConfig.instance.generateNotificationDetails(styleInformation: const DefaultStyleInformation(true, true)), + payload: payload + ); + } + } + + Future removeNotification(String id) async { + if (id.startsWith(FcmUtils.instance.platformOS)) { + await removeNotificationBadgeForIOS(); + } + return _localNotificationsPlugin.cancel(id.hashCode); } void groupPushNotification() async { @@ -168,7 +191,7 @@ class LocalNotificationManager { ); await _localNotificationsPlugin.show( - 1995, + LocalNotificationConfig.groupNotificationId, null, null, LocalNotificationConfig.instance.generateNotificationDetails( @@ -179,6 +202,17 @@ class LocalNotificationManager { } } + void removeGroupPushNotification() async { + final activeNotifications = await _localNotificationsPlugin + .resolvePlatformSpecificImplementation() + ?.getActiveNotifications(); + log('LocalNotificationManager::removeGroupPushNotification():activeNotifications: ${activeNotifications?.length}'); + if (activeNotifications == null || activeNotifications.length <= 1) { + log('LocalNotificationManager::groupPushNotification():canceled'); + await _localNotificationsPlugin.cancel(LocalNotificationConfig.groupNotificationId); + } + } + Future recreateStreamController() { if (localNotificationsController.isClosed) { localNotificationsController = StreamController.broadcast(); @@ -189,4 +223,18 @@ class LocalNotificationManager { void closeStream() { localNotificationsController.close(); } + + Future setNotificationBadgeForIOS() async { + log("LocalNotificationManager::setNotificationBadgeForIOS:"); + if (PlatformInfo.isIOS) { + await FlutterAppBadger.updateBadgeCount(1); + } + } + + Future removeNotificationBadgeForIOS() async { + log("LocalNotificationManager::removeNotificationBadgeForIOS:"); + if (PlatformInfo.isIOS) { + await FlutterAppBadger.removeBadge(); + } + } } \ No newline at end of file diff --git a/lib/features/push_notification/presentation/services/fcm_receiver.dart b/lib/features/push_notification/presentation/services/fcm_receiver.dart index 5103cdfecc..dd8c0b83a1 100644 --- a/lib/features/push_notification/presentation/services/fcm_receiver.dart +++ b/lib/features/push_notification/presentation/services/fcm_receiver.dart @@ -1,8 +1,8 @@ import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:tmail_ui_user/features/push_notification/presentation/services/fcm_service.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/utils/fcm_utils.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; @pragma('vm:entry-point') @@ -26,11 +26,15 @@ class FcmReceiver { FirebaseMessaging.onBackgroundMessage(handleFirebaseBackgroundMessage); } + void onMessageOpenedApp() { + FirebaseMessaging.onMessageOpenedApp.listen(FcmService.instance.handleFirebaseMessageOpenedApp); + } + void getFcmToken() async { try { final currentToken = await FirebaseMessaging.instance.getToken(vapidKey: AppUtils.fcmVapidPublicKey); log('FcmReceiver::onFcmToken():currentToken: $currentToken'); - if (BuildUtils.isWeb) { + if (!FcmUtils.instance.isMobileAndroid) { FcmService.instance.handleGetToken(currentToken); } } catch(e) { diff --git a/lib/features/push_notification/presentation/services/fcm_service.dart b/lib/features/push_notification/presentation/services/fcm_service.dart index 1c565f208c..b1d99131c0 100644 --- a/lib/features/push_notification/presentation/services/fcm_service.dart +++ b/lib/features/push_notification/presentation/services/fcm_service.dart @@ -3,7 +3,8 @@ import 'dart:async'; import 'package:core/utils/app_logger.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_controller.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/controller/fcm_message_controller.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_manager.dart'; class FcmService { @@ -37,18 +38,23 @@ class FcmService { } void handleFirebaseBackgroundMessage(RemoteMessage newRemoteMessage) { - FcmController.instance.initialize(); + FcmMessageController.instance.initialize(); if (!backgroundMessageStreamController.isClosed) { backgroundMessageStreamController.add(newRemoteMessage); } } + void handleFirebaseMessageOpenedApp(RemoteMessage newRemoteMessage) async { + log("FcmService::handleFirebaseMessageOpenedApp:"); + await LocalNotificationManager.instance.removeNotificationBadgeForIOS(); + } + void handleGetToken(String? currentToken) async { log('FcmService::handleGetToken():currentToken: $currentToken'); if (fcmTokenStreamController.isClosed) { log('FcmService::handleGetToken():fcmTokenStreamController: isClosed'); fcmTokenStreamController = StreamController.broadcast(); - await FcmController.instance.listenTokenStream(); + await FcmMessageController.instance.listenTokenStream(); } if (!fcmTokenStreamController.isClosed) { fcmTokenStreamController.add(currentToken); @@ -60,7 +66,7 @@ class FcmService { if (fcmTokenStreamController.isClosed) { log('FcmService::handleRefreshToken():fcmTokenStreamController: isClosed'); fcmTokenStreamController = StreamController.broadcast(); - await FcmController.instance.listenTokenStream(); + await FcmMessageController.instance.listenTokenStream(); } if (!fcmTokenStreamController.isClosed) { fcmTokenStreamController.add(newToken); @@ -70,15 +76,15 @@ class FcmService { Future recreateStreamController() async { if (foregroundMessageStreamController.isClosed) { foregroundMessageStreamController = StreamController.broadcast(); - await FcmController.instance.listenForegroundMessageStream(); + await FcmMessageController.instance.listenForegroundMessageStream(); } if (backgroundMessageStreamController.isClosed) { backgroundMessageStreamController = StreamController.broadcast(); - await FcmController.instance.listenBackgroundMessageStream(); + await FcmMessageController.instance.listenBackgroundMessageStream(); } if (fcmTokenStreamController.isClosed) { fcmTokenStreamController = StreamController.broadcast(); - await FcmController.instance.listenTokenStream(); + await FcmMessageController.instance.listenTokenStream(); } return Future.value(); } diff --git a/lib/features/push_notification/presentation/utils/fcm_utils.dart b/lib/features/push_notification/presentation/utils/fcm_utils.dart index c63ec40964..b720574333 100644 --- a/lib/features/push_notification/presentation/utils/fcm_utils.dart +++ b/lib/features/push_notification/presentation/utils/fcm_utils.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:fcm/model/type_name.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -81,7 +81,7 @@ class FcmUtils { String get platformOS { var platformName = ''; - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { platformName = 'Web'; } else { if (Platform.isAndroid) { @@ -108,4 +108,6 @@ class FcmUtils { log('FcmUtils::hashCodeTokenToDeviceId():deviceId: $deviceId'); return deviceId; } + + bool get isMobileAndroid => PlatformInfo.isMobile && Platform.isAndroid; } \ No newline at end of file diff --git a/lib/features/quotas/data/datasource/quotas_data_source.dart b/lib/features/quotas/data/datasource/quotas_data_source.dart index e0a251649b..a7b5f24229 100644 --- a/lib/features/quotas/data/datasource/quotas_data_source.dart +++ b/lib/features/quotas/data/datasource/quotas_data_source.dart @@ -1,6 +1,6 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:tmail_ui_user/features/quotas/domain/model/quotas_response.dart'; +import 'package:jmap_dart_client/jmap/quotas/quota.dart'; abstract class QuotasDataSource { - Future getQuotas(AccountId accountId); + Future> getQuotas(AccountId accountId); } diff --git a/lib/features/quotas/data/datasource_impl/quotas_data_source_impl.dart b/lib/features/quotas/data/datasource_impl/quotas_data_source_impl.dart index 438709ce28..99abaaeea4 100644 --- a/lib/features/quotas/data/datasource_impl/quotas_data_source_impl.dart +++ b/lib/features/quotas/data/datasource_impl/quotas_data_source_impl.dart @@ -1,21 +1,19 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/quotas/quota.dart'; import 'package:tmail_ui_user/features/quotas/data/datasource/quotas_data_source.dart'; import 'package:tmail_ui_user/features/quotas/data/network/quotas_api.dart'; -import 'package:tmail_ui_user/features/quotas/domain/model/quotas_response.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; -class QuotasDataSourceImpl extends QuotasDataSource{ +class QuotasDataSourceImpl extends QuotasDataSource { final QuotasAPI _quotasAPI; final ExceptionThrower _exceptionThrower; QuotasDataSourceImpl(this._quotasAPI, this._exceptionThrower); @override - Future getQuotas(AccountId accountId) { + Future> getQuotas(AccountId accountId) { return Future.sync(() async { return await _quotasAPI.getQuotas(accountId); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/quotas/data/network/quotas_api.dart b/lib/features/quotas/data/network/quotas_api.dart index c5d30da397..935757785c 100644 --- a/lib/features/quotas/data/network/quotas_api.dart +++ b/lib/features/quotas/data/network/quotas_api.dart @@ -3,36 +3,30 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/jmap_request.dart'; import 'package:jmap_dart_client/jmap/quotas/get/get_quota_method.dart'; import 'package:jmap_dart_client/jmap/quotas/get/get_quota_response.dart'; -import 'package:tmail_ui_user/features/quotas/domain/model/exceptions/quotas_exception.dart'; -import 'package:tmail_ui_user/features/quotas/domain/model/quotas_response.dart'; +import 'package:jmap_dart_client/jmap/quotas/quota.dart'; +import 'package:tmail_ui_user/features/quotas/domain/exceptions/quotas_exception.dart'; class QuotasAPI { final HttpClient _httpClient; QuotasAPI(this._httpClient); - Future getQuotas(AccountId accountId) async { - final requestBuilder = - JmapRequestBuilder(_httpClient, ProcessingInvocation()); + Future> getQuotas(AccountId accountId) async { + final requestBuilder = JmapRequestBuilder(_httpClient, ProcessingInvocation()); final getQuotaMethod = GetQuotaMethod(accountId); final getQuotaInvocation = requestBuilder.invocation(getQuotaMethod); final response = await (requestBuilder - ..usings(getQuotaMethod.requiredCapabilities)) - .build() - .execute(); + ..usings(getQuotaMethod.requiredCapabilities)) + .build() + .execute(); final getQuotaResponse = response.parse( getQuotaInvocation.methodCallId, GetQuotaResponse.deserialize, ); - if(getQuotaResponse != null) { - return QuotasResponse( - accountId: getQuotaResponse.accountId, - notFound: getQuotaResponse.notFound, - quotas: getQuotaResponse.list, - state: getQuotaResponse.state, - ); + if (getQuotaResponse?.list.isNotEmpty == true) { + return getQuotaResponse!.list; } else { throw NotFoundQuotasException(); } diff --git a/lib/features/quotas/data/repository/quotas_repository_impl.dart b/lib/features/quotas/data/repository/quotas_repository_impl.dart index 9a26366613..fcc77ed3a8 100644 --- a/lib/features/quotas/data/repository/quotas_repository_impl.dart +++ b/lib/features/quotas/data/repository/quotas_repository_impl.dart @@ -1,6 +1,6 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/quotas/quota.dart'; import 'package:tmail_ui_user/features/quotas/data/datasource/quotas_data_source.dart'; -import 'package:tmail_ui_user/features/quotas/domain/model/quotas_response.dart'; import 'package:tmail_ui_user/features/quotas/domain/repository/quotas_repository.dart'; class QuotasRepositoryImpl extends QuotasRepository { @@ -9,7 +9,7 @@ class QuotasRepositoryImpl extends QuotasRepository { QuotasRepositoryImpl(this._dataSource); @override - Future getQuotas(AccountId accountId) { + Future> getQuotas(AccountId accountId) { return _dataSource.getQuotas(accountId); } } diff --git a/lib/features/quotas/domain/model/exceptions/quotas_exception.dart b/lib/features/quotas/domain/exceptions/quotas_exception.dart similarity index 100% rename from lib/features/quotas/domain/model/exceptions/quotas_exception.dart rename to lib/features/quotas/domain/exceptions/quotas_exception.dart diff --git a/lib/features/quotas/domain/extensions/list_quotas_extensions.dart b/lib/features/quotas/domain/extensions/list_quotas_extensions.dart new file mode 100644 index 0000000000..e6de29d549 --- /dev/null +++ b/lib/features/quotas/domain/extensions/list_quotas_extensions.dart @@ -0,0 +1,16 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:jmap_dart_client/jmap/quotas/data_types.dart'; +import 'package:jmap_dart_client/jmap/quotas/quota.dart'; + +extension ListQuotasExtensions on List { + + Quota? get octetsQuota { + try { + return firstWhere((quota) => quota.resourceType == ResourceType.octets); + } catch(e) { + logError('ListQuotasExtensions::octetsQuota: Not found octets quota'); + return null; + } + } +} \ No newline at end of file diff --git a/lib/features/quotas/domain/extensions/quota_extensions.dart b/lib/features/quotas/domain/extensions/quota_extensions.dart new file mode 100644 index 0000000000..7d679427f4 --- /dev/null +++ b/lib/features/quotas/domain/extensions/quota_extensions.dart @@ -0,0 +1,131 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:filesize/filesize.dart'; +import 'package:flutter/material.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/quotas/quota.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +extension QuotasExtensions on Quota { + + UnsignedInt? get presentationHardLimit => hardLimit ?? limit; + + String get usedStorageAsString => used != null ? filesize(used!.value) : ''; + + String get hardLimitStorageAsString => presentationHardLimit != null ? filesize(presentationHardLimit!.value) : ''; + + bool get isWarnLimitReached { + if (used != null && warnLimit != null) { + return used!.value >= warnLimit!.value * 0.9; + } else { + return false; + } + } + + bool get isHardLimitReached { + if (used != null && presentationHardLimit != null) { + return used!.value >= presentationHardLimit!.value; + } else { + return false; + } + } + + double get usedStoragePercent { + if (used != null && hardLimit != null && hardLimit!.value > 0) { + return used!.value / hardLimit!.value; + } else { + return 0; + } + } + + bool get allowedDisplayToQuotaBanner => storageAvailable && (isHardLimitReached || isWarnLimitReached); + + bool get storageAvailable => used != null && presentationHardLimit != null; + + String getQuotasStateTitle(BuildContext context) { + if (isHardLimitReached) { + return AppLocalizations.of(context).textQuotasOutOfStorage; + } else { + return AppLocalizations.of(context).quotaStateLabel(usedStorageAsString, hardLimitStorageAsString); + } + } + + Color getQuotasStateTitleColor() { + if (isHardLimitReached) { + return AppColor.colorQuotaError; + } else { + return AppColor.colorLabelQuotas; + } + } + + Color getQuotasStateProgressBarColor() { + if (isHardLimitReached) { + return AppColor.colorQuotaError; + } else if (isWarnLimitReached) { + return AppColor.colorBackgroundQuotasWarning; + } else { + return AppColor.primaryColor; + } + } + + Color getQuotaBannerBackgroundColor() { + if (isHardLimitReached) { + return AppColor.colorQuotaError.withOpacity(0.12); + } else if (isWarnLimitReached) { + return AppColor.colorBackgroundQuotasWarning.withOpacity(0.12); + } else { + return AppColor.colorNetworkConnectionBannerBackground; + } + } + + String getQuotaBannerIcon(ImagePaths imagePaths) { + if (isHardLimitReached) { + return imagePaths.icQuotasOutOfStorage; + } else if (isWarnLimitReached) { + return imagePaths.icQuotasWarning; + } else { + return ''; + } + } + + String getQuotaBannerTitle(BuildContext context) { + if (isHardLimitReached) { + return AppLocalizations.of(context).quotaErrorBannerTitle; + } else if (isWarnLimitReached) { + return AppLocalizations.of(context).quotaWarningBannerTitle; + } else { + return ''; + } + } + + String getQuotaBannerMessage(BuildContext context) { + if (isHardLimitReached) { + return AppLocalizations.of(context).quotaErrorBannerMessage; + } else if (isWarnLimitReached) { + return AppLocalizations.of(context).quotaWarningBannerMessage; + } else { + return ''; + } + } + + Color getQuotaBannerTitleColor() { + if (isHardLimitReached) { + return AppColor.colorQuotaError; + } else if (isWarnLimitReached) { + return AppColor.colorQuotaWarning; + } else { + return Colors.black; + } + } + + Color getQuotaBannerMessageColor() { + if (isHardLimitReached) { + return AppColor.colorQuotaError; + } else if (isWarnLimitReached) { + return AppColor.colorQuotaWarning; + } else { + return Colors.black; + } + } +} \ No newline at end of file diff --git a/lib/features/quotas/domain/model/quotas_response.dart b/lib/features/quotas/domain/model/quotas_response.dart deleted file mode 100644 index 7aa01e2399..0000000000 --- a/lib/features/quotas/domain/model/quotas_response.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:jmap_dart_client/jmap/core/id.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart'; -import 'package:jmap_dart_client/jmap/quotas/quota.dart'; - -class QuotasResponse with EquatableMixin { - final AccountId accountId; - final State state; - final List quotas; - final List? notFound; - - QuotasResponse({ - required this.accountId, - required this.state, - required this.quotas, - this.notFound, - }); - - bool hasData() { - return quotas.isNotEmpty; - } - - @override - List get props => [ - quotas, - state, - notFound, - ]; -} diff --git a/lib/features/quotas/domain/repository/quotas_repository.dart b/lib/features/quotas/domain/repository/quotas_repository.dart index 9f60543f64..bc73d12f3d 100644 --- a/lib/features/quotas/domain/repository/quotas_repository.dart +++ b/lib/features/quotas/domain/repository/quotas_repository.dart @@ -1,6 +1,6 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:tmail_ui_user/features/quotas/domain/model/quotas_response.dart'; +import 'package:jmap_dart_client/jmap/quotas/quota.dart'; abstract class QuotasRepository { - Future getQuotas(AccountId accountId); + Future> getQuotas(AccountId accountId); } diff --git a/lib/features/quotas/domain/state/get_quotas_state.dart b/lib/features/quotas/domain/state/get_quotas_state.dart index bce2b99231..d32bf15a89 100644 --- a/lib/features/quotas/domain/state/get_quotas_state.dart +++ b/lib/features/quotas/domain/state/get_quotas_state.dart @@ -1,25 +1,19 @@ -import 'package:core/core.dart'; -import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/quotas/quota.dart'; +class GetQuotasLoading extends LoadingState {} + class GetQuotasSuccess extends UIState { final List quotas; - final State? state; - GetQuotasSuccess(this.quotas, this.state); + GetQuotasSuccess(this.quotas); @override - List get props => [ - quotas, - state, - ]; + List get props => [quotas]; } class GetQuotasFailure extends FeatureFailure { - final dynamic exception; - - GetQuotasFailure(this.exception); - @override - List get props => [exception]; + GetQuotasFailure(dynamic exception) : super(exception: exception); } diff --git a/lib/features/quotas/domain/use_case/get_quotas_interactor.dart b/lib/features/quotas/domain/use_case/get_quotas_interactor.dart index cc325bcfdd..78cf6727e9 100644 --- a/lib/features/quotas/domain/use_case/get_quotas_interactor.dart +++ b/lib/features/quotas/domain/use_case/get_quotas_interactor.dart @@ -1,6 +1,5 @@ -import 'dart:core'; - -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:tmail_ui_user/features/quotas/domain/repository/quotas_repository.dart'; @@ -13,9 +12,9 @@ class GetQuotasInteractor { Stream> execute(AccountId accountId) async* { try { - yield Right(LoadingState()); - final response = await quotasRepository.getQuotas(accountId); - yield Right(GetQuotasSuccess(response.quotas, response.state)); + yield Right(GetQuotasLoading()); + final listQuotas = await quotasRepository.getQuotas(accountId); + yield Right(GetQuotasSuccess(listQuotas)); } catch (exception) { yield Left(GetQuotasFailure(exception)); } diff --git a/lib/features/quotas/presentation/model/quotas_state.dart b/lib/features/quotas/presentation/model/quotas_state.dart deleted file mode 100644 index 237ba8dd40..0000000000 --- a/lib/features/quotas/presentation/model/quotas_state.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:core/core.dart'; -import 'package:flutter/widgets.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -enum QuotasState { - notAvailable, - normal, - runningOutOfStorage, - runOutOfStorage; - - Color getColorProgress() { - switch(this) { - case QuotasState.notAvailable: - case QuotasState.normal: - return AppColor.primaryColor; - case QuotasState.runningOutOfStorage: - return AppColor.colorProgressQuotasWarning; - case QuotasState.runOutOfStorage: - return AppColor.colorOutOfStorageQuotasWarning; - } - } - - Color getColorQuotasFooterText() { - switch(this) { - case QuotasState.notAvailable: - case QuotasState.normal: - case QuotasState.runningOutOfStorage: - return AppColor.loginTextFieldHintColor; - case QuotasState.runOutOfStorage: - return AppColor.colorOutOfStorageQuotasWarning; - } - } - - String getQuotasFooterText(BuildContext context, num usedCapacity, num softLimitCapacity) { - switch(this) { - case QuotasState.notAvailable: - case QuotasState.normal: - case QuotasState.runningOutOfStorage: - return AppLocalizations.of(context).textQuotasUsed( - usedCapacity.toDouble(), - softLimitCapacity.toDouble(), - ); - case QuotasState.runOutOfStorage: - return AppLocalizations.of(context).textQuotasOutOfStorage; - } - } - - String getIconWarningBanner(ImagePaths imagePaths) { - switch(this) { - case QuotasState.notAvailable: - case QuotasState.normal: - case QuotasState.runningOutOfStorage: - return imagePaths.icQuotasWarning; - case QuotasState.runOutOfStorage: - return imagePaths.icQuotasOutOfStorage; - } - } - - Color getBackgroundColorWarningBanner() { - switch(this) { - case QuotasState.notAvailable: - case QuotasState.normal: - case QuotasState.runningOutOfStorage: - return AppColor.colorBackgroundQuotasWarning.withOpacity(0.12); - case QuotasState.runOutOfStorage: - return AppColor.colorOutOfStorageQuotasWarning.withOpacity(0.12); - } - } - - String getTitleWarningBanner(BuildContext context, num progress) { - switch(this) { - case QuotasState.notAvailable: - case QuotasState.normal: - case QuotasState.runningOutOfStorage: - return AppLocalizations.of(context).textQuotasRunningOutOfStorageTitle(progress.toDouble() * 100); - case QuotasState.runOutOfStorage: - return AppLocalizations.of(context).textQuotasRunOutOfStorageTitle; - } - } - - String getContentWarningBanner(BuildContext context) { - switch(this) { - case QuotasState.notAvailable: - case QuotasState.normal: - case QuotasState.runningOutOfStorage: - return AppLocalizations.of(context).textQuotasRunningOutOfStorageContent; - case QuotasState.runOutOfStorage: - return AppLocalizations.of(context).textQuotasRunOutOfStorageContent; - } - } -} \ No newline at end of file diff --git a/lib/features/quotas/presentation/quotas_controller_bindings.dart b/lib/features/quotas/presentation/quotas_bindings.dart similarity index 96% rename from lib/features/quotas/presentation/quotas_controller_bindings.dart rename to lib/features/quotas/presentation/quotas_bindings.dart index 1c3fd540bb..ed57fd337a 100644 --- a/lib/features/quotas/presentation/quotas_controller_bindings.dart +++ b/lib/features/quotas/presentation/quotas_bindings.dart @@ -9,7 +9,8 @@ import 'package:tmail_ui_user/features/quotas/domain/use_case/get_quotas_interac import 'package:tmail_ui_user/features/quotas/presentation/quotas_controller.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; -class QuotasControllerBindings extends BaseBindings { +class QuotasBindings extends BaseBindings { + @override void bindingsController() { Get.put(QuotasController(Get.find())); @@ -39,5 +40,4 @@ class QuotasControllerBindings extends BaseBindings { void bindingsRepositoryImpl() { Get.lazyPut(() => QuotasRepositoryImpl(Get.find())); } - } \ No newline at end of file diff --git a/lib/features/quotas/presentation/quotas_controller.dart b/lib/features/quotas/presentation/quotas_controller.dart index 71d87ab562..7971765306 100644 --- a/lib/features/quotas/presentation/quotas_controller.dart +++ b/lib/features/quotas/presentation/quotas_controller.dart @@ -1,93 +1,47 @@ -import 'package:core/core.dart'; -import 'package:core/utils/double_convert.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; -import 'package:jmap_dart_client/jmap/core/session/session.dart'; -import 'package:jmap_dart_client/jmap/quotas/data_types.dart'; +import 'package:jmap_dart_client/jmap/quotas/quota.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/quotas/domain/extensions/list_quotas_extensions.dart'; import 'package:tmail_ui_user/features/quotas/domain/state/get_quotas_state.dart'; import 'package:tmail_ui_user/features/quotas/domain/use_case/get_quotas_interactor.dart'; -import 'package:tmail_ui_user/features/quotas/presentation/model/quotas_state.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; class QuotasController extends BaseController { - final MailboxDashBoardController mailboxDashBoardController = Get.find(); - final GetQuotasInteractor _getQuotasInteractor; - final usedCapacity = Rx(0); - final limitCapacity = Rx(0); - final warningLimitCapacity = Rx(0); - final quotasState = QuotasState.notAvailable.obs; - final warningProgressConstant = 0.9; - late Worker accountIdWorker; - double get progressUsedCapacity => limitCapacity.value != 0 - ? (usedCapacity.value / limitCapacity.value) - : 0; + final mailboxDashBoardController = Get.find(); + final responsiveUtils = Get.find(); + final imagePaths = Get.find(); - bool get enableShowWarningQuotas => - quotasState.value != QuotasState.notAvailable && - (quotasState.value == QuotasState.runningOutOfStorage - || quotasState.value == QuotasState.runOutOfStorage); + final GetQuotasInteractor _getQuotasInteractor; - final ImagePaths imagePaths = Get.find(); - final ResponsiveUtils responsiveUtils = Get.find(); + final octetsQuota = Rxn(); - QuotasController(this._getQuotasInteractor); + late Worker accountIdListener; - void _getQuotasAction(AccountId accountId, Session session) { - try { - requireCapability(session, accountId, [CapabilityIdentifier.jmapQuota]); - consumeState(_getQuotasInteractor.execute(mailboxDashBoardController.accountId.value!)); - } catch (e) { - logError('QuotasController::_getQuotasAction():$e'); - } - } + QuotasController(this._getQuotasInteractor); - @override - void onDone() { - viewState.value.fold( - (failure) { - if (failure is GetQuotasFailure) { - logError('QuotasController::onDone():[GetQuotasFailure]: ${failure.exception}'); - } - }, - (success) { - if (success is GetQuotasSuccess) { - _handleGetQuotasSuccess(success); - } - } - ); + void _getQuotasAction(AccountId accountId) { + consumeState(_getQuotasInteractor.execute(accountId)); } void _handleGetQuotasSuccess(GetQuotasSuccess success) { - try { - final quotas = success.quotas.firstWhere((e) => e.resourceType == ResourceType.octets); - usedCapacity.value = DoubleConvert.bytesToGigaBytes(quotas.used.value); - warningLimitCapacity.value = DoubleConvert.bytesToGigaBytes(quotas.limit.value * warningProgressConstant); - limitCapacity.value = DoubleConvert.bytesToGigaBytes(quotas.limit.value); - if(usedCapacity.value >= limitCapacity.value) { - quotasState.value = QuotasState.runOutOfStorage; - } else if (usedCapacity.value >= warningLimitCapacity.value) { - quotasState.value = QuotasState.runningOutOfStorage; - } else { - quotasState.value = QuotasState.normal; - } - } catch (e) { - quotasState.value = QuotasState.notAvailable; - logError('QuotasController::_handleGetQuotasSuccess():[NotFoundException]: $e'); - } - } - - void covertBytesToGB() { - + octetsQuota.value = success.quotas.octetsQuota; } void _initWorker() { - accountIdWorker = ever(mailboxDashBoardController.accountId, (accountId) { - if (accountId is AccountId && mailboxDashBoardController.sessionCurrent!= null) { - _getQuotasAction(accountId, mailboxDashBoardController.sessionCurrent!); + accountIdListener = ever(mailboxDashBoardController.accountId, (accountId) { + final session = mailboxDashBoardController.sessionCurrent; + if (accountId is AccountId && + session != null && + CapabilityIdentifier.jmapQuota.isSupported(session, accountId) + ) { + _getQuotasAction(accountId); } }); } @@ -100,7 +54,15 @@ class QuotasController extends BaseController { @override void onClose() { - accountIdWorker.call(); + accountIdListener.dispose(); super.onClose(); } + + @override + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetQuotasSuccess) { + _handleGetQuotasSuccess(success); + } + } } diff --git a/lib/features/quotas/presentation/quotas_view.dart b/lib/features/quotas/presentation/quotas_view.dart new file mode 100644 index 0000000000..0143887e88 --- /dev/null +++ b/lib/features/quotas/presentation/quotas_view.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/quotas/domain/extensions/quota_extensions.dart'; +import 'package:tmail_ui_user/features/quotas/presentation/quotas_controller.dart'; +import 'package:tmail_ui_user/features/quotas/presentation/styles/quotas_view_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class QuotasView extends GetWidget { + + const QuotasView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Obx(() { + if (controller.octetsQuota.value != null && controller.octetsQuota.value!.storageAvailable) { + final octetQuota = controller.octetsQuota.value!; + return LayoutBuilder(builder: (context, constraints) { + return Container( + padding: const EdgeInsetsDirectional.only( + start: QuotasViewStyles.padding, + top: QuotasViewStyles.padding, + bottom: QuotasViewStyles.bottomPadding + ), + margin: controller.responsiveUtils.isWebDesktop(context) + ? const EdgeInsetsDirectional.only(end: QuotasViewStyles.margin) + : null, + decoration: BoxDecoration( + color: controller.responsiveUtils.isWebDesktop(context) + ? QuotasViewStyles.webBackgroundColor + : QuotasViewStyles.mobileBackgroundColor, + border: const Border( + top: BorderSide( + color: QuotasViewStyles.topLineColor, + width: QuotasViewStyles.topLineSize, + ) + ), + ), + alignment: AlignmentDirectional.centerStart, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + SvgPicture.asset( + controller.imagePaths.icQuotas, + width: QuotasViewStyles.iconSize, + height: QuotasViewStyles.iconSize, + fit: BoxFit.fill, + ), + const SizedBox(width: QuotasViewStyles.iconPadding), + Text( + AppLocalizations.of(context).storageQuotas, + style: const TextStyle( + fontSize: QuotasViewStyles.labelTextSize, + fontWeight: QuotasViewStyles.labelFontWeight, + color: QuotasViewStyles.labelTextColor + ), + ) + ], + ), + const SizedBox(height: QuotasViewStyles.space), + SizedBox( + width: _getProgressBarMaxWith(constraints.maxWidth), + child: LinearProgressIndicator( + color: octetQuota.getQuotasStateProgressBarColor(), + minHeight: QuotasViewStyles.progressBarHeight, + backgroundColor: QuotasViewStyles.progressBarBackgroundColor, + value: octetQuota.usedStoragePercent, + ), + ), + const SizedBox(height: QuotasViewStyles.space), + Text( + octetQuota.getQuotasStateTitle(context), + style: TextStyle( + fontSize: QuotasViewStyles.progressStateTextSize, + fontWeight: QuotasViewStyles.progressStateFontWeight, + color: octetQuota.getQuotasStateTitleColor() + ), + ) + ], + ), + ); + }); + } else { + return const SizedBox.shrink(); + } + }); + } + + double _getProgressBarMaxWith(double maxWith) { + if (maxWith > QuotasViewStyles.progressBarMaxWidth) { + return QuotasViewStyles.progressBarMaxWidth; + } else { + return maxWith; + } + } +} diff --git a/lib/features/quotas/presentation/styles/quotas_banner_styles.dart b/lib/features/quotas/presentation/styles/quotas_banner_styles.dart new file mode 100644 index 0000000000..7a0e698e64 --- /dev/null +++ b/lib/features/quotas/presentation/styles/quotas_banner_styles.dart @@ -0,0 +1,23 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class QuotasBannerStyles { + static const double verticalPadding = 12; + static const double horizontalPadding = 16; + static const double topMargin = 8; + static const double startMargin = 16; + static const double endMargin = 16; + static const double bottomMargin = 8; + static const double iconPadding = 16; + static const double iconSize = 32; + static const double titleTextSize = 17; + static const double messageTextSize = 15; + static const double space = 8; + static const double borderRadius = 12; + + static const Color messageTextColor = AppColor.colorLabelQuotas; + + static const FontWeight titleFontWeight = FontWeight.w700; + static const FontWeight messageFontWeight = FontWeight.w400; +} \ No newline at end of file diff --git a/lib/features/quotas/presentation/styles/quotas_view_styles.dart b/lib/features/quotas/presentation/styles/quotas_view_styles.dart new file mode 100644 index 0000000000..c53be437a6 --- /dev/null +++ b/lib/features/quotas/presentation/styles/quotas_view_styles.dart @@ -0,0 +1,26 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class QuotasViewStyles { + static const double padding = 16; + static const double margin = 16; + static const double bottomPadding = 24; + static const double iconPadding = 12; + static const double iconSize = 24; + static const double labelTextSize = 13; + static const double space = 8; + static const double progressBarHeight = 3; + static const double progressBarMaxWidth = 168; + static const double progressStateTextSize = 13; + static const double topLineSize = 1; + + static const Color labelTextColor = AppColor.colorLabelQuotas; + static const Color webBackgroundColor = AppColor.colorBgDesktop; + static const Color mobileBackgroundColor = Colors.white; + static const Color progressBarBackgroundColor = AppColor.colorDivider; + static const Color topLineColor = AppColor.colorDividerHorizontal; + + static const FontWeight labelFontWeight = FontWeight.w400; + static const FontWeight progressStateFontWeight = FontWeight.w400; +} \ No newline at end of file diff --git a/lib/features/quotas/presentation/widget/quotas_banner_widget.dart b/lib/features/quotas/presentation/widget/quotas_banner_widget.dart new file mode 100644 index 0000000000..fdfda656f5 --- /dev/null +++ b/lib/features/quotas/presentation/widget/quotas_banner_widget.dart @@ -0,0 +1,77 @@ +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/quotas/domain/extensions/quota_extensions.dart'; +import 'package:tmail_ui_user/features/quotas/presentation/quotas_controller.dart'; +import 'package:tmail_ui_user/features/quotas/presentation/styles/quotas_banner_styles.dart'; + +class QuotasBannerWidget extends StatelessWidget { + + const QuotasBannerWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final controller = Get.find(); + final responsiveUtils = Get.find(); + return Obx(() { + if (controller.octetsQuota.value != null && controller.octetsQuota.value!.allowedDisplayToQuotaBanner) { + final octetQuota = controller.octetsQuota.value!; + return Container( + decoration: BoxDecoration( + color: octetQuota.getQuotaBannerBackgroundColor(), + borderRadius: const BorderRadius.all(Radius.circular(QuotasBannerStyles.borderRadius)), + ), + margin: EdgeInsetsDirectional.only( + end: QuotasBannerStyles.endMargin, + top: PlatformInfo.isWeb ? QuotasBannerStyles.topMargin : 0, + start: responsiveUtils.isWebDesktop(context) ? 0 : QuotasBannerStyles.startMargin, + bottom: responsiveUtils.isWebDesktop(context) ? 0 : QuotasBannerStyles.bottomMargin + ), + padding: const EdgeInsetsDirectional.symmetric( + horizontal: QuotasBannerStyles.horizontalPadding, + vertical: QuotasBannerStyles.verticalPadding, + ), + child: Row( + children: [ + SvgPicture.asset( + octetQuota.getQuotaBannerIcon(controller.imagePaths), + width: QuotasBannerStyles.iconSize, + height: QuotasBannerStyles.iconSize, + fit: BoxFit.fill, + ), + const SizedBox(width: QuotasBannerStyles.iconPadding), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + octetQuota.getQuotaBannerTitle(context), + style: TextStyle( + fontSize: QuotasBannerStyles.titleTextSize, + fontWeight: QuotasBannerStyles.titleFontWeight, + color: octetQuota.getQuotaBannerTitleColor(), + ), + ), + const SizedBox(height: QuotasBannerStyles.space), + Text( + octetQuota.getQuotaBannerMessage(context), + style: const TextStyle( + fontSize: QuotasBannerStyles.messageTextSize, + fontWeight: QuotasBannerStyles.messageFontWeight, + color: QuotasBannerStyles.messageTextColor, + ), + ), + ], + ), + ), + ], + ), + ); + } else { + return const SizedBox.shrink(); + } + }); + } +} diff --git a/lib/features/quotas/presentation/widget/quotas_footer_widget.dart b/lib/features/quotas/presentation/widget/quotas_footer_widget.dart deleted file mode 100644 index 0755b93a40..0000000000 --- a/lib/features/quotas/presentation/widget/quotas_footer_widget.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/quotas/presentation/model/quotas_state.dart'; -import 'package:tmail_ui_user/features/quotas/presentation/quotas_controller.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -class QuotasFooterWidget extends GetWidget { - - const QuotasFooterWidget({ - Key? key, - this.padding - }) : super(key: key); - - final EdgeInsets? padding; - - @override - Widget build(BuildContext context) { - return Obx( - () => controller.quotasState.value != QuotasState.notAvailable - ? Container( - color: AppColor.colorBgDesktop, - padding: padding ?? const EdgeInsets.symmetric(vertical: 12, horizontal: 24), - alignment: Alignment.centerLeft, - child: IntrinsicWidth( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - SvgPicture.asset(controller.imagePaths.icQuotas), - const SizedBox(width: 12), - Text( - AppLocalizations.of(context).storageQuotas, - style: const TextStyle( - fontSize: 13, - fontWeight: FontWeight.w400, - color: AppColor.loginTextFieldHintColor), - ) - ], - ), - const SizedBox(height: 8), - LinearProgressIndicator( - color: controller.quotasState.value.getColorProgress(), - minHeight: 3, - backgroundColor: AppColor.colorDivider, - value: controller.progressUsedCapacity, - ), - const SizedBox(height: 8), - Text( - controller.quotasState.value.getQuotasFooterText( - context, - controller.usedCapacity.value, - controller.limitCapacity.value, - ), - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w400, - color: controller.quotasState.value.getColorQuotasFooterText(), - ), - ) - ], - ), - ), - ) - : const SizedBox.shrink(), - ); - } -} diff --git a/lib/features/quotas/presentation/widget/quotas_warning_banner_widget.dart b/lib/features/quotas/presentation/widget/quotas_warning_banner_widget.dart deleted file mode 100644 index f8359f5cd4..0000000000 --- a/lib/features/quotas/presentation/widget/quotas_warning_banner_widget.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/quotas/presentation/quotas_controller.dart'; - -class QuotasWarningBannerWidget extends GetWidget { - const QuotasWarningBannerWidget({this.margin ,Key? key}) : super(key: key); - final EdgeInsetsGeometry? margin; - - @override - Widget build(BuildContext context) { - return Obx( - () => controller.enableShowWarningQuotas - ? Container( - padding: const EdgeInsets.all(16), - margin: margin ?? const EdgeInsets.only(left: 12, right: 12, top: 8), - decoration: BoxDecoration( - color: controller.quotasState.value.getBackgroundColorWarningBanner(), - borderRadius: const BorderRadius.all(Radius.circular(10)), - ), - child: Row( - children: [ - SvgPicture.asset(controller.quotasState.value.getIconWarningBanner(controller.imagePaths)), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - controller.quotasState.value.getTitleWarningBanner( - context, - controller.progressUsedCapacity, - ), - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.w700, - color: AppColor.colorTitleQuotasWarning, - ), - ), - const SizedBox(height: 8), - Text( - controller.quotasState.value.getContentWarningBanner(context), - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w400, - color: AppColor.loginTextFieldHintColor, - ), - ), - ], - ), - ), - ], - ), - ) - : const SizedBox.shrink(), - ); - } -} diff --git a/lib/features/rules_filter_creator/presentation/model/rule_filter_condition_type.dart b/lib/features/rules_filter_creator/presentation/model/rule_filter_condition_type.dart new file mode 100644 index 0000000000..a8984f187b --- /dev/null +++ b/lib/features/rules_filter_creator/presentation/model/rule_filter_condition_type.dart @@ -0,0 +1,5 @@ +enum RuleFilterConditionScreenType { + mobile, + tablet, + desktop +} \ No newline at end of file diff --git a/lib/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart b/lib/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart index b3a2b15cf5..4c3eaedb78 100644 --- a/lib/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart +++ b/lib/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:rule_filter/rule_filter/tmail_rule.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/creator_action_type.dart'; @@ -12,13 +13,26 @@ class RulesFilterCreatorArguments with EquatableMixin { final CreatorActionType actionType; final TMailRule? tMailRule; final EmailAddress? emailAddress; + final PresentationMailbox? mailboxDestination; - RulesFilterCreatorArguments(this.accountId, this.session, { - this.actionType = CreatorActionType.create, - this.tMailRule, - this.emailAddress - }); + RulesFilterCreatorArguments( + this.accountId, + this.session, + { + this.actionType = CreatorActionType.create, + this.tMailRule, + this.emailAddress, + this.mailboxDestination, + } + ); @override - List get props => [accountId, actionType, session, tMailRule, emailAddress]; + List get props => [ + accountId, + actionType, + session, + tMailRule, + emailAddress, + mailboxDestination, + ]; } \ No newline at end of file diff --git a/lib/features/rules_filter_creator/presentation/model/rules_filter_input_field_arguments.dart b/lib/features/rules_filter_creator/presentation/model/rules_filter_input_field_arguments.dart new file mode 100644 index 0000000000..eefbb82d37 --- /dev/null +++ b/lib/features/rules_filter_creator/presentation/model/rules_filter_input_field_arguments.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; + +class RulesFilterInputFieldArguments with EquatableMixin { + final FocusNode focusNode; + final String errorText; + final TextEditingController controller; + + RulesFilterInputFieldArguments({ + required this.focusNode, + required this.errorText, + required this.controller, + }); + + @override + List get props => [ + focusNode, + errorText, + controller, + ]; +} diff --git a/lib/features/rules_filter_creator/presentation/rules_filter_creator_bindings.dart b/lib/features/rules_filter_creator/presentation/rules_filter_creator_bindings.dart index 5dee9046a2..b97a5d59ef 100644 --- a/lib/features/rules_filter_creator/presentation/rules_filter_creator_bindings.dart +++ b/lib/features/rules_filter_creator/presentation/rules_filter_creator_bindings.dart @@ -1,7 +1,7 @@ import 'package:core/data/model/source_type/data_source_type.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart'; @@ -79,8 +79,4 @@ class RulesFilterCreatorBindings extends BaseBindings { Get.find(), )); } - - void dispose() { - Get.delete(); - } } \ No newline at end of file diff --git a/lib/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart b/lib/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart index 885cb24354..062c7a90ae 100644 --- a/lib/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart +++ b/lib/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart @@ -1,11 +1,10 @@ -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/presentation/utils/keyboard_utils.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; -import 'package:dartz/dartz.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -16,11 +15,13 @@ import 'package:model/model.dart'; import 'package:rule_filter/rule_filter/rule_action.dart'; import 'package:rule_filter/rule_filter/rule_append_in.dart'; import 'package:rule_filter/rule_filter/rule_condition.dart' as rule_condition; +import 'package:rule_filter/rule_filter/rule_condition.dart'; import 'package:rule_filter/rule_filter/tmail_rule.dart'; import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/model/verification/empty_name_validator.dart'; @@ -37,9 +38,10 @@ import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/e import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_input_field_arguments.dart'; -typedef OnCreatedRuleFilterCallback = Function(dynamic arguments); class RulesFilterCreatorController extends BaseMailboxController { @@ -50,30 +52,25 @@ class RulesFilterCreatorController extends BaseMailboxController { GetAllRulesInteractor? _getAllRulesInteractor; final errorRuleName = Rxn(); - final errorRuleConditionValue = Rxn(); final errorRuleActionValue = Rxn(); - final ruleConditionFieldSelected = Rxn(); - final ruleConditionComparatorSelected = Rxn(); final emailRuleFilterActionSelected = Rxn(); final mailboxSelected = Rxn(); final actionType = CreatorActionType.create.obs; + final listRuleCondition = RxList(); - TextEditingController? inputRuleNameController; - TextEditingController? inputConditionValueController; - FocusNode? inputRuleNameFocusNode; - FocusNode? inputRuleConditionFocusNode; + final TextEditingController inputRuleNameController = TextEditingController(); + final FocusNode inputRuleNameFocusNode = FocusNode(); + final listRuleConditionValueArguments = RxList(); String? _newRuleName; - String? _newRuleConditionValue; RulesFilterCreatorArguments? arguments; - OnCreatedRuleFilterCallback? onCreatedRuleFilterCallback; - VoidCallback? onDismissRuleFilterCreator; AccountId? _accountId; Session? _session; TMailRule? _currentTMailRule; EmailAddress? _emailAddress; List? _listEmailRule; + PresentationMailbox? _mailboxDestination; RulesFilterCreatorController( this._getAllMailboxInteractor, @@ -84,21 +81,20 @@ class RulesFilterCreatorController extends BaseMailboxController { @override void onInit() { super.onInit(); - inputRuleNameController = TextEditingController(); - inputConditionValueController = TextEditingController(); - inputRuleNameFocusNode = FocusNode(); - inputRuleConditionFocusNode = FocusNode(); - + log('RulesFilterCreatorController::onInit():arguments: ${Get.arguments}'); + arguments = Get.arguments; } @override void onReady() { super.onReady(); + log('RulesFilterCreatorController::onReady():'); if (arguments != null) { _accountId = arguments!.accountId; _session = arguments!.session; _currentTMailRule = arguments!.tMailRule; _emailAddress = arguments!.emailAddress; + _mailboxDestination = arguments!.mailboxDestination; actionType.value = arguments!.actionType; injectRuleFilterBindings(_session, _accountId); try { @@ -113,33 +109,31 @@ class RulesFilterCreatorController extends BaseMailboxController { @override void onClose() { - _disposeWidget(); + log('RulesFilterCreatorController::onClose():'); + inputRuleNameFocusNode.dispose(); + inputRuleNameController.dispose(); + for (var ruleConditionValueArguments in listRuleConditionValueArguments) { + ruleConditionValueArguments.focusNode.dispose(); + ruleConditionValueArguments.controller.dispose(); + } super.onClose(); } @override - void onDone() { - viewState.value.fold((failure) {}, (success) { - if (success is GetAllRulesSuccess) { - log('RulesFilterCreatorController::onDone():GetAllRulesSuccess: ${success.rules}'); - if (success.rules?.isNotEmpty == true) { - _listEmailRule = success.rules!; - } + void handleSuccessViewState(Success success) async { + super.handleSuccessViewState(success); + if (success is GetAllMailboxSuccess) { + await buildTree(success.mailboxList); + _setUpMailboxSelected(); + if (currentContext != null) { + await syncAllMailboxWithDisplayName(currentContext!); } - }); - } - - @override - void onData(Either newState) { - super.onData(newState); - newState.fold( - (failure) => null, - (success) async { - if (success is GetAllMailboxSuccess) { - await buildTree(success.mailboxList); - _setUpMailboxSelected(); - } - }); + } else if (success is GetAllRulesSuccess) { + log('RulesFilterCreatorController::handleSuccessViewState():GetAllRulesSuccess: ${success.rules}'); + if (success.rules?.isNotEmpty == true) { + _listEmailRule = success.rules!; + } + } } void _getAllRules() { @@ -151,28 +145,62 @@ class RulesFilterCreatorController extends BaseMailboxController { void _setUpDefaultValueRuleFilter() { switch(actionType.value) { case CreatorActionType.create: - ruleConditionFieldSelected.value = rule_condition.Field.from; - ruleConditionComparatorSelected.value = rule_condition.Comparator.contains; + RuleCondition newRuleCondition = RuleCondition( + field: rule_condition.Field.from, + comparator: rule_condition.Comparator.contains, + value: '' + ); + listRuleCondition.add(newRuleCondition); + RulesFilterInputFieldArguments newRuleConditionValueArguments = RulesFilterInputFieldArguments( + focusNode: FocusNode(), + errorText: '', + controller: TextEditingController(), + ); + listRuleConditionValueArguments.add(newRuleConditionValueArguments); emailRuleFilterActionSelected.value = EmailRuleFilterAction.moveMessage; if (_emailAddress != null) { - _newRuleConditionValue = _emailAddress?.email; - _setValueInputField(inputConditionValueController, _newRuleConditionValue ?? ''); + RuleCondition firstRuleCondition = RuleCondition( + field: rule_condition.Field.from, + comparator: rule_condition.Comparator.contains, + value: _emailAddress!.email!, + ); + listRuleCondition[0] = firstRuleCondition; + listRuleCondition.refresh(); + _setValueInputField( + listRuleConditionValueArguments[0].controller, + listRuleCondition[0].value + ); + } + if (_mailboxDestination != null) { + mailboxSelected.value = _mailboxDestination; } break; case CreatorActionType.edit: if (_currentTMailRule != null) { - ruleConditionFieldSelected.value = _currentTMailRule!.condition.field; - ruleConditionComparatorSelected.value = _currentTMailRule!.condition.comparator; + RuleCondition currentRule = RuleCondition( + field: _currentTMailRule!.condition.field, + comparator: _currentTMailRule!.condition.comparator, + value: _currentTMailRule!.condition.value + ); + listRuleCondition.add(currentRule); + RulesFilterInputFieldArguments newRuleConditionValueArguments = RulesFilterInputFieldArguments( + focusNode: FocusNode(), + errorText: '', + controller: TextEditingController(), + ); + listRuleConditionValueArguments.add(newRuleConditionValueArguments); emailRuleFilterActionSelected.value = EmailRuleFilterAction.moveMessage; - _newRuleConditionValue = _currentTMailRule!.condition.value; - _setValueInputField(inputConditionValueController, _newRuleConditionValue ?? ''); + _setValueInputField( + listRuleConditionValueArguments[0].controller, + listRuleCondition[0].value + ); _newRuleName = _currentTMailRule!.name; _setValueInputField(inputRuleNameController, _newRuleName ?? ''); _getAllMailboxAction(); } break; } - inputRuleNameFocusNode?.requestFocus(); + inputRuleNameFocusNode.requestFocus(); } void _setValueInputField(TextEditingController? controller, String value) { @@ -200,9 +228,26 @@ class RulesFilterCreatorController extends BaseMailboxController { errorRuleName.value = _getErrorStringByInputValue(context, _newRuleName); } - void updateConditionValue(BuildContext context, String? value) { - _newRuleConditionValue = value; - errorRuleConditionValue.value = _getErrorStringByInputValue(context, _newRuleConditionValue); + void updateConditionValue(BuildContext context, String? value, int ruleConditionIndex) { + RuleCondition newRuleCondition = RuleCondition( + field: listRuleCondition[ruleConditionIndex].field, + comparator: listRuleCondition[ruleConditionIndex].comparator, + value: value!, + ); + listRuleCondition[ruleConditionIndex] = newRuleCondition; + listRuleCondition.refresh(); + String? errorString = _getErrorStringByInputValue(context, listRuleCondition[ruleConditionIndex].value); + RulesFilterInputFieldArguments newRuleConditionValueArguments = RulesFilterInputFieldArguments( + focusNode: listRuleConditionValueArguments[ruleConditionIndex].focusNode, + errorText: errorString ?? '', + controller: listRuleConditionValueArguments[ruleConditionIndex].controller, + ); + if (listRuleConditionValueArguments.length > ruleConditionIndex) { + listRuleConditionValueArguments[ruleConditionIndex] = newRuleConditionValueArguments; + } else { + listRuleConditionValueArguments.add(newRuleConditionValueArguments); + } + listRuleConditionValueArguments.refresh(); } String? _getErrorStringByInputValue(BuildContext context, String? inputValue) { @@ -218,12 +263,28 @@ class RulesFilterCreatorController extends BaseMailboxController { ); } - void selectRuleConditionField(rule_condition.Field? newField) { - ruleConditionFieldSelected.value = newField; + void selectRuleConditionField(rule_condition.Field? newField, int? ruleConditionIndex) { + if (newField != null && ruleConditionIndex != null) { + RuleCondition newRuleCondition = RuleCondition( + field: newField, + comparator: listRuleCondition[ruleConditionIndex].comparator, + value: listRuleCondition[ruleConditionIndex].value, + ); + listRuleCondition[ruleConditionIndex] = newRuleCondition; + listRuleCondition.refresh(); + } } - void selectRuleConditionComparator(rule_condition.Comparator? newComparator) { - ruleConditionComparatorSelected.value = newComparator; + void selectRuleConditionComparator(rule_condition.Comparator? newComparator, int? ruleConditionIndex) { + if (newComparator != null && ruleConditionIndex != null) { + RuleCondition newRuleCondition = RuleCondition( + field: listRuleCondition[ruleConditionIndex].field, + comparator: newComparator, + value: listRuleCondition[ruleConditionIndex].value, + ); + listRuleCondition[ruleConditionIndex] = newRuleCondition; + listRuleCondition.refresh(); + } } void selectEmailRuleFilterAction(EmailRuleFilterAction? newAction) { @@ -233,73 +294,81 @@ class RulesFilterCreatorController extends BaseMailboxController { void selectMailbox(BuildContext context) async { if (_accountId != null) { final arguments = DestinationPickerArguments( - _accountId!, - MailboxActions.selectForRuleAction, - _session); - - if (BuildUtils.isWeb) { - showDialogDestinationPicker( - context: context, - arguments: arguments, - onSelectedMailbox: (destinationMailbox) { - mailboxSelected.value = destinationMailbox; - errorRuleActionValue.value = _getErrorStringByInputValue( - context, - mailboxSelected.value?.name?.name); - }); - } else { - final destinationMailbox = await push( - AppRoutes.destinationPicker, - arguments: arguments); - - if (destinationMailbox is PresentationMailbox) { - mailboxSelected.value = destinationMailbox; - errorRuleActionValue.value = _getErrorStringByInputValue( - context, - mailboxSelected.value?.name?.name); - } + _accountId!, + MailboxActions.selectForRuleAction, + _session); + + final destinationMailbox = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments) + : await push(AppRoutes.destinationPicker, arguments: arguments); + + if (destinationMailbox is PresentationMailbox && context.mounted) { + mailboxSelected.value = destinationMailbox; + errorRuleActionValue.value = _getErrorStringByInputValue( + context, + mailboxSelected.value?.getDisplayName(context)); } } } - void createNewRuleFilter(BuildContext context) async { - FocusScope.of(context).unfocus(); + void createNewRuleFilter(BuildContext context) { + KeyboardUtils.hideKeyboard(context); final errorName = _getErrorStringByInputValue(context, _newRuleName); + log('RulesFilterCreatorController::createNewRuleFilter:errorName: $errorName'); if (errorName?.isNotEmpty == true) { errorRuleName.value = errorName; - inputRuleNameFocusNode?.requestFocus(); + inputRuleNameFocusNode.requestFocus(); return; } - final errorCondition = _getErrorStringByInputValue(context, _newRuleConditionValue); - if (errorCondition?.isNotEmpty == true) { - errorRuleConditionValue.value = errorCondition; - inputRuleConditionFocusNode?.requestFocus(); - return; + if (listRuleCondition.isNotEmpty) { + String? errorConditionString; + for (var ruleCondition in listRuleCondition) { + errorConditionString = _getErrorStringByInputValue(context, ruleCondition.value); + log('RulesFilterCreatorController::createNewRuleFilter:errorConditionString: $errorConditionString'); + if (errorConditionString != null) { + int ruleConditionIndex = listRuleCondition.indexOf(ruleCondition); + RulesFilterInputFieldArguments newRuleConditionValueArguments = RulesFilterInputFieldArguments( + focusNode: listRuleConditionValueArguments[ruleConditionIndex].focusNode, + errorText: errorConditionString, + controller: listRuleConditionValueArguments[ruleConditionIndex].controller, + ); + listRuleConditionValueArguments[ruleConditionIndex] = newRuleConditionValueArguments; + listRuleConditionValueArguments[listRuleCondition.indexOf(ruleCondition)].focusNode.requestFocus(); + } + } + if (errorConditionString?.isNotEmpty == true) { + return; + } } - final errorAction = _getErrorStringByInputValue(context, mailboxSelected.value?.name?.name); + final errorAction = _getErrorStringByInputValue(context, mailboxSelected.value?.getDisplayName(context)); + log('RulesFilterCreatorController::createNewRuleFilter:errorAction: $errorAction'); if (errorAction?.isNotEmpty == true) { errorRuleActionValue.value = errorAction; - _appToast.showToastWithIcon( + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( currentOverlayContext!, - textColor: AppColor.toastErrorBackgroundColor, - message: AppLocalizations.of(currentContext!).this_field_cannot_be_blank); + AppLocalizations.of(currentContext!).this_field_cannot_be_blank); + } return; } - if (ruleConditionFieldSelected.value == null || - ruleConditionComparatorSelected.value == null || - emailRuleFilterActionSelected.value == null) { - _appToast.showToastWithIcon( + if (listRuleCondition.isEmpty || emailRuleFilterActionSelected.value == null) { + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastErrorMessage( currentOverlayContext!, - textColor: AppColor.toastErrorBackgroundColor, - message: AppLocalizations.of(currentContext!).toastErrorMessageWhenCreateNewRule); + AppLocalizations.of(currentContext!).toastErrorMessageWhenCreateNewRule); + } return; } - final newTMailRule = TMailRule( + late EquatableMixin ruleFilterRequest; + + if (actionType.value == CreatorActionType.create) { + for (var ruleCondition in listRuleCondition) { + final newTMailRule = TMailRule( id: _currentTMailRule?.id, name: _newRuleName!, action: RuleAction( @@ -308,61 +377,52 @@ class RulesFilterCreatorController extends BaseMailboxController { ) ), condition: rule_condition.RuleCondition( - field: ruleConditionFieldSelected.value!, - comparator: ruleConditionComparatorSelected.value!, - value: _newRuleConditionValue! + field: ruleCondition.field, + comparator: ruleCondition.comparator, + value: ruleCondition.value )); - - if (actionType.value == CreatorActionType.create) { - final ruleFilterRequest = CreateNewEmailRuleFilterRequest( - _listEmailRule ?? [], - newTMailRule); - - if (BuildUtils.isWeb) { - _disposeWidget(); - onCreatedRuleFilterCallback?.call(ruleFilterRequest); - } else { - popBack(result: ruleFilterRequest); + ruleFilterRequest = CreateNewEmailRuleFilterRequest(_listEmailRule ?? [], newTMailRule); } } else { - final ruleFilterRequest = EditEmailRuleFilterRequest( - _listEmailRule?.withIds ?? [], - newTMailRule); - - if (BuildUtils.isWeb) { - _disposeWidget(); - onCreatedRuleFilterCallback?.call(ruleFilterRequest); - } else { - popBack(result: ruleFilterRequest); - } + final newTMailRule = TMailRule( + id: _currentTMailRule?.id, + name: _newRuleName!, + action: RuleAction( + appendIn: RuleAppendIn( + mailboxIds: [mailboxSelected.value!.id] + ) + ), + condition: rule_condition.RuleCondition( + field: listRuleCondition[0].field, + comparator: listRuleCondition[0].comparator, + value: listRuleCondition[0].value + )); + ruleFilterRequest = EditEmailRuleFilterRequest(_listEmailRule?.withIds ?? [], newTMailRule); } + popBack(result: ruleFilterRequest); } - void _clearAll() { - inputRuleNameController?.clear(); - inputConditionValueController?.clear(); + void closeView(BuildContext context) { + KeyboardUtils.hideKeyboard(context); + popBack(); } - void _disposeWidget() { - inputRuleNameFocusNode?.dispose(); - inputRuleNameFocusNode = null; - inputRuleConditionFocusNode?.dispose(); - inputRuleConditionFocusNode = null; - inputRuleNameController?.dispose(); - inputRuleNameController = null; - inputConditionValueController?.dispose(); - inputConditionValueController = null; + void tapAddCondition() { + RuleCondition newRuleCondition = RuleCondition( + field: rule_condition.Field.from, + comparator: rule_condition.Comparator.contains, + value: '' + ); + listRuleCondition.add(newRuleCondition); + listRuleConditionValueArguments.add(RulesFilterInputFieldArguments( + focusNode: FocusNode(), + errorText: '', + controller: TextEditingController(), + )); } - void closeView(BuildContext context) { - _clearAll(); - FocusScope.of(context).unfocus(); - - if (BuildUtils.isWeb) { - _disposeWidget(); - onDismissRuleFilterCreator?.call(); - } else { - popBack(); - } + void tapRemoveCondition(int ruleConditionIndex) { + listRuleCondition.removeAt(ruleConditionIndex); + listRuleConditionValueArguments.removeAt(ruleConditionIndex); } } \ No newline at end of file diff --git a/lib/features/rules_filter_creator/presentation/rules_filter_creator_view.dart b/lib/features/rules_filter_creator/presentation/rules_filter_creator_view.dart index 4c4b6390cb..f257593ea5 100644 --- a/lib/features/rules_filter_creator/presentation/rules_filter_creator_view.dart +++ b/lib/features/rules_filter_creator/presentation/rules_filter_creator_view.dart @@ -8,16 +8,18 @@ import 'package:rule_filter/rule_filter/rule_condition.dart' as rule_condition; import 'package:tmail_ui_user/features/base/widget/drop_down_button_widget.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/extensions/rule_condition_extensions.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/email_rule_filter_action.dart'; -import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/rules_filter_creator_controller.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/widgets/rule_action_bottom_sheet_action_tile_builder.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/widgets/rule_condition_comparator_bottom_sheet_action_tile_builder.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/widgets/rule_condition_field_bottom_sheet_action_tile_builder.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/widgets/rule_filter_button_field.dart'; +import 'package:tmail_ui_user/features/rules_filter_creator/presentation/widgets/rule_filter_condition_widget.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/widgets/rules_filter_input_field_builder.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; +import 'model/rule_filter_condition_type.dart'; + class RuleFilterCreatorView extends GetWidget { final _imagePaths = Get.find(); @@ -26,20 +28,7 @@ class RuleFilterCreatorView extends GetWidget { @override final controller = Get.find(); - RuleFilterCreatorView({Key? key}) : super(key: key) { - controller.arguments = Get.arguments; - } - - RuleFilterCreatorView.fromArguments( - RulesFilterCreatorArguments arguments, { - Key? key, - OnCreatedRuleFilterCallback? onCreatedRuleFilterCallback, - VoidCallback? onDismissCallback - }) : super(key: key) { - controller.arguments = arguments; - controller.onCreatedRuleFilterCallback = onCreatedRuleFilterCallback; - controller.onDismissRuleFilterCreator = onDismissCallback; - } + RuleFilterCreatorView({super.key}); @override Widget build(BuildContext context) { @@ -47,7 +36,7 @@ class RuleFilterCreatorView extends GetWidget { child: ResponsiveWidget( responsiveUtils: _responsiveUtils, mobile: Scaffold( - backgroundColor: BuildUtils.isWeb + backgroundColor: PlatformInfo.isWeb ? Colors.black.withAlpha(24) : Colors.black38, body: GestureDetector( @@ -62,7 +51,7 @@ class RuleFilterCreatorView extends GetWidget { topLeft: Radius.circular(16), topRight: Radius.circular(16)), color: Colors.white), - margin: const EdgeInsets.only(top: BuildUtils.isWeb ? 70 : 0), + margin: const EdgeInsets.only(top: PlatformInfo.isWeb ? 70 : 0), child: ClipRRect( borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), @@ -157,40 +146,32 @@ class RuleFilterCreatorView extends GetWidget { fontSize: 16, color: Colors.black)), const SizedBox(height: 24), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColor.colorBackgroundFieldConditionRulesFilter, - borderRadius: BorderRadius.circular(12)), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: Obx(() => DropDownButtonWidget( - items: rule_condition.Field.values, - itemSelected: controller.ruleConditionFieldSelected.value, - dropdownMaxHeight: 250, - onChanged: (newField) => - controller.selectRuleConditionField(newField), - supportSelectionIcon: true))), - Container( - width: 220, - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Obx(() => DropDownButtonWidget( - items: rule_condition.Comparator.values, - itemSelected: controller.ruleConditionComparatorSelected.value, - onChanged: (newComparator) => - controller.selectRuleConditionComparator(newComparator), - supportSelectionIcon: true))), - Expanded(child: Obx(() => RulesFilterInputField( - hintText: AppLocalizations.of(context).conditionValueHintTextInput, - errorText: controller.errorRuleConditionValue.value, - editingController: controller.inputConditionValueController, - focusNode: controller.inputRuleConditionFocusNode, - onChangeAction: (value) => - controller.updateConditionValue(context, value)))) - ] - ) - ), + _buildListRuleFilterConditionList(context, RuleFilterConditionScreenType.desktop), + // Container( + // padding: const EdgeInsets.only(top: 8), + // child: InkWell( + // onTap: controller.tapAddCondition, + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // SvgPicture.asset( + // _imagePaths.icAddNewFolder, + // fit: BoxFit.fill, + // ), + // const SizedBox(width: 15,), + // Text( + // AppLocalizations.of(context).addCondition, + // maxLines: 1, + // style: const TextStyle( + // fontWeight: FontWeight.w500, + // fontSize: 17, + // color: AppColor.primaryColor + // ) + // ) + // ], + // ), + // ), + // ), const SizedBox(height: 24), Text(AppLocalizations.of(context).actionTitleRulesFilter, overflow: CommonTextStyle.defaultTextOverFlow, @@ -230,7 +211,7 @@ class RuleFilterCreatorView extends GetWidget { value: controller.mailboxSelected.value, borderColor: _getBorderColorMailboxSelected(), tapActionCallback: (value) { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); controller.selectMailbox(context); }))), ]) @@ -272,7 +253,7 @@ class RuleFilterCreatorView extends GetWidget { Positioned(top: 8, right: 8, child: buildIconWeb( icon: SvgPicture.asset( - _imagePaths.icCloseMailbox, + _imagePaths.icCircleClose, fit: BoxFit.fill), tooltip: AppLocalizations.of(context).close, onTap: () => controller.closeView(context))) @@ -316,40 +297,32 @@ class RuleFilterCreatorView extends GetWidget { fontSize: 16, color: Colors.black)), const SizedBox(height: 24), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColor.colorBackgroundFieldConditionRulesFilter, - borderRadius: BorderRadius.circular(12)), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded(child: Obx(() => DropDownButtonWidget( - items: rule_condition.Field.values, - itemSelected: controller.ruleConditionFieldSelected.value, - dropdownMaxHeight: 250, - onChanged: (newField) => - controller.selectRuleConditionField(newField), - supportSelectionIcon: true))), - Container( - width: 220, - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Obx(() => DropDownButtonWidget( - items: rule_condition.Comparator.values, - itemSelected: controller.ruleConditionComparatorSelected.value, - onChanged: (newComparator) => - controller.selectRuleConditionComparator(newComparator), - supportSelectionIcon: true))), - Expanded(child: Obx(() => RulesFilterInputField( - hintText: AppLocalizations.of(context).conditionValueHintTextInput, - errorText: controller.errorRuleConditionValue.value, - editingController: controller.inputConditionValueController, - focusNode: controller.inputRuleConditionFocusNode, - onChangeAction: (value) => - controller.updateConditionValue(context, value)))) - ] - ) - ), + _buildListRuleFilterConditionList(context, RuleFilterConditionScreenType.tablet), + // Container( + // padding: const EdgeInsets.only(top: 8), + // child: InkWell( + // onTap: controller.tapAddCondition, + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // children: [ + // SvgPicture.asset( + // _imagePaths.icAddNewFolder, + // fit: BoxFit.fill, + // ), + // const SizedBox(width: 15,), + // Text( + // AppLocalizations.of(context).addCondition, + // maxLines: 1, + // style: const TextStyle( + // fontWeight: FontWeight.w500, + // fontSize: 17, + // color: AppColor.primaryColor + // ) + // ) + // ], + // ), + // ), + // ), const SizedBox(height: 24), Text(AppLocalizations.of(context).actionTitleRulesFilter, overflow: CommonTextStyle.defaultTextOverFlow, @@ -390,7 +363,7 @@ class RuleFilterCreatorView extends GetWidget { value: controller.mailboxSelected.value, borderColor: _getBorderColorMailboxSelected(), tapActionCallback: (value) { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); controller.selectMailbox(context); }))), ]) @@ -431,7 +404,7 @@ class RuleFilterCreatorView extends GetWidget { Positioned(top: 8, right: 8, child: buildIconWeb( icon: SvgPicture.asset( - _imagePaths.icCloseMailbox, + _imagePaths.icCircleClose, fit: BoxFit.fill), tooltip: AppLocalizations.of(context).close, onTap: () => controller.closeView(context))) @@ -481,53 +454,33 @@ class RuleFilterCreatorView extends GetWidget { fontSize: 16, color: Colors.black)), const SizedBox(height: 24), - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: AppColor.colorBackgroundFieldConditionRulesFilter, - borderRadius: BorderRadius.circular(12)), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Obx(() { - return RuleFilterButtonField( - value: controller.ruleConditionFieldSelected.value, - tapActionCallback: (value) { - FocusScope.of(context).unfocus(); - controller.openContextMenuAction( - context, - _bottomSheetRuleConditionFieldActionTiles( - context, - controller.ruleConditionFieldSelected.value)); - } - ); - }), - Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: Obx(() { - return RuleFilterButtonField( - value: controller.ruleConditionComparatorSelected.value, - tapActionCallback: (value) { - FocusScope.of(context).unfocus(); - controller.openContextMenuAction( - context, - _bottomSheetRuleConditionComparatorActionTiles( - context, - controller.ruleConditionComparatorSelected.value)); - } - ); - }), - ), - Obx(() => RulesFilterInputField( - hintText: AppLocalizations.of(context).conditionValueHintTextInput, - errorText: controller.errorRuleConditionValue.value, - editingController: controller.inputConditionValueController, - focusNode: controller.inputRuleConditionFocusNode, - onChangeAction: (value) => - controller.updateConditionValue(context, value))) - ] - ) - ), + _buildListRuleFilterConditionList(context, RuleFilterConditionScreenType.mobile), + // Container( + // padding: const EdgeInsets.only(top: 12), + // child: InkWell( + // onTap: controller.tapAddCondition, + // child: Row( + // crossAxisAlignment: CrossAxisAlignment.center, + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // SvgPicture.asset( + // _imagePaths.icAddNewFolder, + // fit: BoxFit.fill, + // ), + // const SizedBox(width: 15,), + // Text( + // AppLocalizations.of(context).addCondition, + // maxLines: 1, + // style: const TextStyle( + // fontWeight: FontWeight.w500, + // fontSize: 17, + // color: AppColor.primaryColor + // ) + // ) + // ], + // ), + // ), + // ), const Padding( padding: EdgeInsets.symmetric(vertical: 12), child: Divider( @@ -553,7 +506,7 @@ class RuleFilterCreatorView extends GetWidget { return RuleFilterButtonField( value: controller.emailRuleFilterActionSelected.value, tapActionCallback: (value) { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); controller.openContextMenuAction( context, _bottomSheetActionRuleFilterActionTiles( @@ -579,7 +532,7 @@ class RuleFilterCreatorView extends GetWidget { value: controller.mailboxSelected.value, borderColor: _getBorderColorMailboxSelected(), tapActionCallback: (value) { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); controller.selectMailbox(context); })) ]) @@ -620,7 +573,7 @@ class RuleFilterCreatorView extends GetWidget { Positioned(top: 8, right: 8, child: buildIconWeb( icon: SvgPicture.asset( - _imagePaths.icCloseMailbox, + _imagePaths.icCircleClose, fit: BoxFit.fill), tooltip: AppLocalizations.of(context).close, onTap: () => controller.closeView(context))) @@ -628,20 +581,79 @@ class RuleFilterCreatorView extends GetWidget { ); } + Widget _buildListRuleFilterConditionList( + BuildContext context, + RuleFilterConditionScreenType ruleFilterConditionScreenType + ) { + return Obx(() { + return ListView.separated( + shrinkWrap: true, + itemCount: controller.listRuleCondition.length, + itemBuilder: (context, index) { + return RuleFilterConditionWidget( + key: ValueKey(controller.listRuleConditionValueArguments[index].focusNode), + ruleFilterConditionScreenType: ruleFilterConditionScreenType, + ruleCondition: controller.listRuleCondition[index], + imagePaths: _imagePaths, + conditionValueErrorText: controller.listRuleConditionValueArguments[index].errorText, + conditionValueFocusNode: controller.listRuleConditionValueArguments[index].focusNode, + conditionValueEditingController: controller.listRuleConditionValueArguments[index].controller, + tapRuleConditionFieldCallback: (value) => { + if (ruleFilterConditionScreenType == RuleFilterConditionScreenType.mobile) { + controller.openContextMenuAction( + context, + _bottomSheetRuleConditionFieldActionTiles( + context, + controller.listRuleCondition[index].field, + index, + ) + ), + } else { + controller.selectRuleConditionField(value, index) + } + }, + tapRuleConditionComparatorCallback: (value) => { + if (ruleFilterConditionScreenType == RuleFilterConditionScreenType.mobile) { + controller.openContextMenuAction( + context, + _bottomSheetRuleConditionComparatorActionTiles( + context, + controller.listRuleCondition[index].comparator, + index, + ) + ), + } else { + controller.selectRuleConditionComparator(value, index), + } + }, + conditionValueOnChangeAction: (value) => + controller.updateConditionValue(context, value, index), + tapRemoveRuleFilterConditionCallback: () => controller.tapRemoveCondition(index), + ); + }, + separatorBuilder: (context, index) { + return const SizedBox(height: 12,); + }, + ); + }); + } + List _bottomSheetRuleConditionFieldActionTiles( BuildContext context, - rule_condition.Field? fieldSelected + rule_condition.Field? fieldSelected, + int? ruleConditionIndex, ) { return rule_condition.Field.values .map((field) => - _buildRuleConditionFieldWidget(context, field, fieldSelected)) + _buildRuleConditionFieldWidget(context, field, fieldSelected, ruleConditionIndex)) .toList(); } Widget _buildRuleConditionFieldWidget( BuildContext context, rule_condition.Field field, - rule_condition.Field? fieldSelected + rule_condition.Field? fieldSelected, + int? ruleConditionIndex, ) { return (RuleConditionFieldSheetActionTileBuilder( field.getTitle(context), @@ -655,7 +667,7 @@ class RuleFilterCreatorView extends GetWidget { height: 20, fit: BoxFit.fill)) ..onActionClick((field) { - controller.selectRuleConditionField(field); + controller.selectRuleConditionField(field, ruleConditionIndex); popBack(); })) .build(); @@ -663,18 +675,20 @@ class RuleFilterCreatorView extends GetWidget { List _bottomSheetRuleConditionComparatorActionTiles( BuildContext context, - rule_condition.Comparator? comparatorSelected + rule_condition.Comparator? comparatorSelected, + int? ruleConditionIndex, ) { return rule_condition.Comparator.values .map((comparator) => - _buildRuleConditionComparatorWidget(context, comparator, comparatorSelected)) + _buildRuleConditionComparatorWidget(context, comparator, comparatorSelected, ruleConditionIndex)) .toList(); } Widget _buildRuleConditionComparatorWidget( BuildContext context, rule_condition.Comparator comparator, - rule_condition.Comparator? comparatorSelected + rule_condition.Comparator? comparatorSelected, + int? ruleConditionIndex, ) { return (RuleConditionComparatorSheetActionTileBuilder( comparator.getTitle(context), @@ -688,7 +702,7 @@ class RuleFilterCreatorView extends GetWidget { height: 20, fit: BoxFit.fill)) ..onActionClick((comparator) { - controller.selectRuleConditionComparator(comparator); + controller.selectRuleConditionComparator(comparator, ruleConditionIndex); popBack(); })) .build(); diff --git a/lib/features/rules_filter_creator/presentation/widgets/rule_action_bottom_sheet_action_tile_builder.dart b/lib/features/rules_filter_creator/presentation/widgets/rule_action_bottom_sheet_action_tile_builder.dart index b3871d1ccb..2c6c6b25e5 100644 --- a/lib/features/rules_filter_creator/presentation/widgets/rule_action_bottom_sheet_action_tile_builder.dart +++ b/lib/features/rules_filter_creator/presentation/widgets/rule_action_bottom_sheet_action_tile_builder.dart @@ -33,7 +33,7 @@ class RuleActionSheetActionTileBuilder return Container( color: bgColor ?? Colors.white, child: MouseRegion( - cursor: BuildUtils.isWeb + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( diff --git a/lib/features/rules_filter_creator/presentation/widgets/rule_condition_comparator_bottom_sheet_action_tile_builder.dart b/lib/features/rules_filter_creator/presentation/widgets/rule_condition_comparator_bottom_sheet_action_tile_builder.dart index 3e871afa6c..c334c9b266 100644 --- a/lib/features/rules_filter_creator/presentation/widgets/rule_condition_comparator_bottom_sheet_action_tile_builder.dart +++ b/lib/features/rules_filter_creator/presentation/widgets/rule_condition_comparator_bottom_sheet_action_tile_builder.dart @@ -33,7 +33,7 @@ class RuleConditionComparatorSheetActionTileBuilder return Container( color: bgColor ?? Colors.white, child: MouseRegion( - cursor: BuildUtils.isWeb + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( diff --git a/lib/features/rules_filter_creator/presentation/widgets/rule_condition_field_bottom_sheet_action_tile_builder.dart b/lib/features/rules_filter_creator/presentation/widgets/rule_condition_field_bottom_sheet_action_tile_builder.dart index aef61c31ef..4136e3d36f 100644 --- a/lib/features/rules_filter_creator/presentation/widgets/rule_condition_field_bottom_sheet_action_tile_builder.dart +++ b/lib/features/rules_filter_creator/presentation/widgets/rule_condition_field_bottom_sheet_action_tile_builder.dart @@ -33,7 +33,7 @@ class RuleConditionFieldSheetActionTileBuilder return Container( color: bgColor ?? Colors.white, child: MouseRegion( - cursor: BuildUtils.isWeb + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( diff --git a/lib/features/rules_filter_creator/presentation/widgets/rule_filter_button_field.dart b/lib/features/rules_filter_creator/presentation/widgets/rule_filter_button_field.dart index e001fa523a..6f1c19d340 100644 --- a/lib/features/rules_filter_creator/presentation/widgets/rule_filter_button_field.dart +++ b/lib/features/rules_filter_creator/presentation/widgets/rule_filter_button_field.dart @@ -7,6 +7,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/model.dart'; import 'package:rule_filter/rule_filter/rule_condition.dart' as rule_condition; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/extensions/rule_condition_extensions.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/email_rule_filter_action.dart'; @@ -27,7 +28,7 @@ class RuleFilterButtonField extends StatelessWidget { @override Widget build(BuildContext context) { - final _imagePaths = Get.find(); + final imagePaths = Get.find(); return InkWell( onTap: () => tapActionCallback?.call(value), @@ -51,7 +52,7 @@ class RuleFilterButtonField extends StatelessWidget { softWrap: CommonTextStyle.defaultSoftWrap, overflow: CommonTextStyle.defaultTextOverFlow, )), - SvgPicture.asset(_imagePaths.icDropDown) + SvgPicture.asset(imagePaths.icDropDown) ]), ), ); @@ -59,7 +60,7 @@ class RuleFilterButtonField extends StatelessWidget { String _getName(BuildContext context, T? value) { if (value is PresentationMailbox) { - return value.name?.name ?? ''; + return value.getDisplayName(context); } if (value is rule_condition.Field) { return value.getTitle(context); diff --git a/lib/features/rules_filter_creator/presentation/widgets/rule_filter_condition_remove_button_builder.dart b/lib/features/rules_filter_creator/presentation/widgets/rule_filter_condition_remove_button_builder.dart new file mode 100644 index 0000000000..24220254b2 --- /dev/null +++ b/lib/features/rules_filter_creator/presentation/widgets/rule_filter_condition_remove_button_builder.dart @@ -0,0 +1,31 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +class RuleFilterConditionRemoveButton extends StatelessWidget { + final Function()? tapRemoveRuleFilterConditionCallback; + final ImagePaths? imagePath; + + const RuleFilterConditionRemoveButton({ + Key? key, + this.tapRemoveRuleFilterConditionCallback, + this.imagePath, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: tapRemoveRuleFilterConditionCallback, + child: CircleAvatar( + backgroundColor: AppColor.colorRemoveRuleFilterConditionButton, + radius: 22, + child: SvgPicture.asset( + imagePath!.icMinimize, + fit: BoxFit.fill, + colorFilter: AppColor.colorDeletePermanentlyButton.asFilter(), + ), + ) + ); + } +} \ No newline at end of file diff --git a/lib/features/rules_filter_creator/presentation/widgets/rule_filter_condition_row_builder.dart b/lib/features/rules_filter_creator/presentation/widgets/rule_filter_condition_row_builder.dart new file mode 100644 index 0000000000..96eb9a5f8c --- /dev/null +++ b/lib/features/rules_filter_creator/presentation/widgets/rule_filter_condition_row_builder.dart @@ -0,0 +1,122 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/keyboard_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:rule_filter/rule_filter/rule_condition.dart'; +import 'package:rule_filter/rule_filter/rule_condition.dart' as rule_condition; +import 'package:tmail_ui_user/features/base/widget/drop_down_button_widget.dart'; +import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rule_filter_condition_type.dart'; +import 'package:tmail_ui_user/features/rules_filter_creator/presentation/widgets/rule_filter_button_field.dart'; +import 'package:tmail_ui_user/features/rules_filter_creator/presentation/widgets/rules_filter_input_field_builder.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class RuleFilterConditionRow extends StatelessWidget { + final RuleFilterConditionScreenType? ruleFilterConditionScreenType; + final RuleCondition ruleCondition; + final Function(Field?)? tapRuleConditionFieldCallback; + final Function(Comparator?)? tapRuleConditionComparatorCallback; + final String? conditionValueErrorText; + final TextEditingController? conditionValueEditingController; + final FocusNode? conditionValueFocusNode; + final OnChangeFilterInputAction? conditionValueOnChangeAction; + final Function()? tapRemoveRuleFilterConditionCallback; + final ImagePaths? imagePaths; + + const RuleFilterConditionRow({ + Key? key, + required this.ruleFilterConditionScreenType, + required this.ruleCondition, + this.tapRuleConditionFieldCallback, + this.tapRuleConditionComparatorCallback, + this.conditionValueErrorText, + this.conditionValueEditingController, + this.conditionValueFocusNode, + this.conditionValueOnChangeAction, + this.tapRemoveRuleFilterConditionCallback, + this.imagePaths, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + switch (ruleFilterConditionScreenType) { + case RuleFilterConditionScreenType.mobile: + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RuleFilterButtonField( + value: ruleCondition.field, + tapActionCallback: (value) { + KeyboardUtils.hideKeyboard(context); + tapRuleConditionFieldCallback!(ruleCondition.field); + }, + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: RuleFilterButtonField( + value: ruleCondition.comparator, + tapActionCallback: (value) { + KeyboardUtils.hideKeyboard(context); + tapRuleConditionComparatorCallback!(ruleCondition.comparator); + }, + ) + ), + RulesFilterInputField( + hintText: AppLocalizations.of(context).conditionValueHintTextInput, + errorText: conditionValueErrorText, + focusNode: conditionValueFocusNode, + onChangeAction: conditionValueOnChangeAction, + editingController: conditionValueEditingController, + ), + ], + ); + case RuleFilterConditionScreenType.tablet: + case RuleFilterConditionScreenType.desktop: + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: DropDownButtonWidget( + items: rule_condition.Field.values, + itemSelected: ruleCondition.field, + dropdownMaxHeight: 250, + onChanged: (newField) => { + tapRuleConditionFieldCallback!(newField) + }, + supportSelectionIcon: true, + ) + ), + Container( + width: 220, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: DropDownButtonWidget( + items: rule_condition.Comparator.values, + itemSelected: ruleCondition.comparator, + onChanged: (newComparator) => { + tapRuleConditionComparatorCallback!(newComparator) + }, + supportSelectionIcon: true, + ) + ), + Expanded( + child: RulesFilterInputField( + hintText: AppLocalizations.of(context).conditionValueHintTextInput, + errorText: conditionValueErrorText, + focusNode: conditionValueFocusNode, + onChangeAction: conditionValueOnChangeAction, + editingController: conditionValueEditingController, + ) + ), + // Container( + // padding: const EdgeInsets.only(left: 12), + // alignment: Alignment.center, + // child: RuleFilterConditionRemoveButton( + // tapRemoveRuleFilterConditionCallback: tapRemoveRuleFilterConditionCallback, + // imagePath: imagePaths, + // ) + // ), + ], + ); + default: + return const SizedBox.shrink(); + } + } +} diff --git a/lib/features/rules_filter_creator/presentation/widgets/rule_filter_condition_widget.dart b/lib/features/rules_filter_creator/presentation/widgets/rule_filter_condition_widget.dart new file mode 100644 index 0000000000..6e6047c406 --- /dev/null +++ b/lib/features/rules_filter_creator/presentation/widgets/rule_filter_condition_widget.dart @@ -0,0 +1,99 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rule_filter_condition_type.dart'; +import 'package:rule_filter/rule_filter/rule_condition.dart'; +import 'package:tmail_ui_user/features/rules_filter_creator/presentation/widgets/rule_filter_condition_row_builder.dart'; +import 'package:tmail_ui_user/features/rules_filter_creator/presentation/widgets/rules_filter_input_field_builder.dart'; +import 'package:core/presentation/resources/image_paths.dart'; + +class RuleFilterConditionWidget extends StatelessWidget { + final RuleFilterConditionScreenType? ruleFilterConditionScreenType; + final RuleCondition ruleCondition; + final Function(Field?)? tapRuleConditionFieldCallback; + final Function(Comparator?)? tapRuleConditionComparatorCallback; + final String? conditionValueErrorText; + final TextEditingController? conditionValueEditingController; + final FocusNode? conditionValueFocusNode; + final OnChangeFilterInputAction? conditionValueOnChangeAction; + final ImagePaths? imagePaths; + final Function()? tapRemoveRuleFilterConditionCallback; + + const RuleFilterConditionWidget({ + super.key, + this.ruleFilterConditionScreenType, + required this.ruleCondition, + this.tapRuleConditionFieldCallback, + this.tapRuleConditionComparatorCallback, + this.conditionValueErrorText, + this.conditionValueEditingController, + this.conditionValueFocusNode, + this.conditionValueOnChangeAction, + this.imagePaths, + this.tapRemoveRuleFilterConditionCallback, + }); + + @override + Widget build(BuildContext context) { + return Slidable( + // enabled: ruleFilterConditionScreenType == RuleFilterConditionScreenType.mobile ? true : false, + enabled: false, + endActionPane: ActionPane( + extentRatio: 0.1, + motion: const BehindMotion(), + children: [ + CustomSlidableAction( + padding: const EdgeInsets.only(right: 12), + borderRadius: const BorderRadius.only(topRight: Radius.circular(12), bottomRight: Radius.circular(12)), + onPressed: (_) => tapRemoveRuleFilterConditionCallback!(), + backgroundColor: AppColor.colorBackgroundFieldConditionRulesFilter, + child: CircleAvatar( + backgroundColor: AppColor.colorRemoveRuleFilterConditionButton, + radius: 110, + child: SvgPicture.asset( + imagePaths!.icMinimize, + fit: BoxFit.fill, + colorFilter: AppColor.colorDeletePermanentlyButton.asFilter(), + ), + ) + ) + ] + ), + child: Builder(builder: (context) { + SlidableController? slideController = Slidable.of(context); + return ValueListenableBuilder( + valueListenable: slideController?.direction ?? ValueNotifier(0), + builder: (context, value, _) { + var borderRadius = value != -1 ? + BorderRadius.circular(12) : + const BorderRadius.only( + bottomLeft: Radius.circular(12), + topLeft: Radius.circular(12) + ); + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColor.colorBackgroundFieldConditionRulesFilter, + borderRadius: borderRadius, + ), + child: RuleFilterConditionRow( + ruleFilterConditionScreenType: ruleFilterConditionScreenType, + ruleCondition: ruleCondition, + tapRuleConditionFieldCallback: tapRuleConditionFieldCallback, + tapRuleConditionComparatorCallback: tapRuleConditionComparatorCallback, + conditionValueErrorText: conditionValueErrorText, + conditionValueEditingController: conditionValueEditingController, + conditionValueFocusNode: conditionValueFocusNode, + conditionValueOnChangeAction: conditionValueOnChangeAction, + tapRemoveRuleFilterConditionCallback: tapRemoveRuleFilterConditionCallback, + imagePaths: imagePaths, + ) + ); + } + ); + }) + ); + + } +} \ No newline at end of file diff --git a/lib/features/rules_filter_creator/presentation/widgets/rules_filter_input_decoration_builder.dart b/lib/features/rules_filter_creator/presentation/widgets/rules_filter_input_decoration_builder.dart index 8c757c4545..cbea00dce7 100644 --- a/lib/features/rules_filter_creator/presentation/widgets/rules_filter_input_decoration_builder.dart +++ b/lib/features/rules_filter_creator/presentation/widgets/rules_filter_input_decoration_builder.dart @@ -41,7 +41,7 @@ class RulesFilterInputDecorationBuilder extends InputDecorationBuilder { contentPadding: contentPadding ?? const EdgeInsets.symmetric( horizontal: 12, vertical: 12), - errorText: errorText, + errorText: errorText != '' ? errorText : null, errorStyle: errorTextStyle ?? const TextStyle( color: AppColor.colorInputBorderErrorVerifyName, fontSize: 13), diff --git a/lib/features/rules_filter_creator/presentation/widgets/rules_filter_input_field_builder.dart b/lib/features/rules_filter_creator/presentation/widgets/rules_filter_input_field_builder.dart index 01bbe2d4b4..175abe76f0 100644 --- a/lib/features/rules_filter_creator/presentation/widgets/rules_filter_input_field_builder.dart +++ b/lib/features/rules_filter_creator/presentation/widgets/rules_filter_input_field_builder.dart @@ -1,6 +1,7 @@ import 'package:core/presentation/views/text/text_field_builder.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/widgets/rules_filter_input_decoration_builder.dart'; @@ -25,20 +26,20 @@ class RulesFilterInputField extends StatelessWidget { @override Widget build(BuildContext context) { - return (TextFieldBuilder() - ..onChange((value) => onChangeAction?.call(value)) - ..textInputAction(TextInputAction.next) - ..addController(editingController ?? TextEditingController()) - ..textStyle(const TextStyle(color: Colors.black, fontSize: 16)) - ..keyboardType(TextInputType.text) - ..addFocusNode(focusNode) - ..textDecoration((RulesFilterInputDecorationBuilder() - ..setContentPadding(const EdgeInsets.symmetric( - vertical: BuildUtils.isWeb ? 16 : 12, - horizontal: 12)) - ..setHintText(hintText) - ..setErrorText(errorText)) - .build())) - .build(); + return TextFieldBuilder( + onTextChange: onChangeAction, + textInputAction: TextInputAction.next, + controller: editingController, + textStyle: const TextStyle(color: Colors.black, fontSize: 16), + textDirection: DirectionUtils.getDirectionByLanguage(context), + keyboardType: TextInputType.text, + focusNode: focusNode, + maxLines: 1, + decoration: (RulesFilterInputDecorationBuilder() + ..setContentPadding(const EdgeInsets.symmetric(vertical: PlatformInfo.isWeb ? 16 : 12, horizontal: 12)) + ..setHintText(hintText) + ..setErrorText(errorText)) + .build(), + ); } } \ No newline at end of file diff --git a/lib/features/search/email/domain/state/refresh_changes_search_email_state.dart b/lib/features/search/email/domain/state/refresh_changes_search_email_state.dart index 8d3339c40e..950e36248e 100644 --- a/lib/features/search/email/domain/state/refresh_changes_search_email_state.dart +++ b/lib/features/search/email/domain/state/refresh_changes_search_email_state.dart @@ -3,13 +3,7 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:model/email/presentation_email.dart'; -class RefreshingChangeSearchEmailState extends UIState { - - RefreshingChangeSearchEmailState(); - - @override - List get props => []; -} +class RefreshingChangeSearchEmailState extends UIState {} class RefreshChangesSearchEmailSuccess extends UIState { final List emailList; @@ -21,10 +15,6 @@ class RefreshChangesSearchEmailSuccess extends UIState { } class RefreshChangesSearchEmailFailure extends FeatureFailure { - final dynamic exception; - - RefreshChangesSearchEmailFailure(this.exception); - @override - List get props => [exception]; + RefreshChangesSearchEmailFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/search/email/domain/usecases/refresh_changes_search_email_interactor.dart b/lib/features/search/email/domain/usecases/refresh_changes_search_email_interactor.dart index 7c36cab070..b783044ab5 100644 --- a/lib/features/search/email/domain/usecases/refresh_changes_search_email_interactor.dart +++ b/lib/features/search/email/domain/usecases/refresh_changes_search_email_interactor.dart @@ -5,6 +5,7 @@ import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:model/extensions/email_extension.dart'; @@ -18,23 +19,25 @@ class RefreshChangesSearchEmailInteractor { RefreshChangesSearchEmailInteractor(this.threadRepository); Stream> execute( - AccountId accountId, - { - UnsignedInt? limit, - Set? sort, - Filter? filter, - Properties? properties, - } + Session session, + AccountId accountId, + { + UnsignedInt? limit, + Set? sort, + Filter? filter, + Properties? properties, + } ) async* { try { yield Right(RefreshingChangeSearchEmailState()); final emailList = await threadRepository.searchEmails( - accountId, - limit: limit, - sort: sort, - filter: filter, - properties: properties); + session, + accountId, + limit: limit, + sort: sort, + filter: filter, + properties: properties); final presentationEmailList = emailList .map((email) => email.toPresentationEmail()) diff --git a/lib/features/search/email/presentation/model/simple_search_filter.dart b/lib/features/search/email/presentation/model/simple_search_filter.dart index a93c9be109..dd51c159ae 100644 --- a/lib/features/search/email/presentation/model/simple_search_filter.dart +++ b/lib/features/search/email/presentation/model/simple_search_filter.dart @@ -9,6 +9,7 @@ import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; import 'package:model/email/prefix_email_address.dart'; import 'package:model/extensions/email_filter_condition_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; @@ -21,6 +22,8 @@ class SimpleSearchFilter with EquatableMixin { final EmailReceiveTimeType emailReceiveTimeType; final bool hasAttachment; final UTCDate? before; + final UTCDate? startDate; + final UTCDate? endDate; SimpleSearchFilter({ Set? from, @@ -30,6 +33,8 @@ class SimpleSearchFilter with EquatableMixin { this.text, this.before, this.mailbox, + this.startDate, + this.endDate }) : from = from ?? {}, to = to ?? {}, hasAttachment = hasAttachment ?? false, @@ -43,6 +48,8 @@ class SimpleSearchFilter with EquatableMixin { Option? emailReceiveTimeTypeOption, Option? hasAttachmentOption, Option? beforeOption, + Option? startDateOption, + Option? endDateOption }) { return SimpleSearchFilter( from: _getOptionParam(fromOption, from), @@ -52,6 +59,8 @@ class SimpleSearchFilter with EquatableMixin { emailReceiveTimeType: _getOptionParam(emailReceiveTimeTypeOption, emailReceiveTimeType), hasAttachment: _getOptionParam(hasAttachmentOption, hasAttachment), before: _getOptionParam(beforeOption, before), + startDate: _getOptionParam(startDateOption, startDate), + endDate: _getOptionParam(endDateOption, endDate) ); } @@ -69,9 +78,9 @@ class SimpleSearchFilter with EquatableMixin { ? text?.value : null, inMailbox: mailbox?.id, - after: emailReceiveTimeType.toUTCDate(), + after: emailReceiveTimeType.getAfterDate(startDate), hasAttachment: hasAttachment == false ? null : hasAttachment, - before: before, + before: emailReceiveTimeType.getBeforeDate(endDate, before) ); final listEmailCondition = { @@ -93,7 +102,17 @@ class SimpleSearchFilter with EquatableMixin { } @override - List get props => [from, to, text, mailbox, emailReceiveTimeType, hasAttachment, before]; + List get props => [ + from, + to, + text, + mailbox, + emailReceiveTimeType, + hasAttachment, + before, + startDate, + endDate + ]; } extension SearchEmailFilterExtension on SimpleSearchFilter { @@ -147,5 +166,5 @@ extension SearchEmailFilterExtension on SimpleSearchFilter { } } - String get mailboxName => mailbox?.name?.name ?? ''; + String getMailboxName(BuildContext context) => mailbox?.getDisplayName(context) ?? ''; } \ No newline at end of file diff --git a/lib/features/search/email/presentation/search_email_controller.dart b/lib/features/search/email/presentation/search_email_controller.dart index 246afe15c8..e438c98340 100644 --- a/lib/features/search/email/presentation/search_email_controller.dart +++ b/lib/features/search/email/presentation/search_email_controller.dart @@ -1,8 +1,9 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/keyboard_utils.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:flutter/material.dart'; @@ -22,17 +23,21 @@ import 'package:model/email/presentation_email.dart'; import 'package:model/email/read_actions.dart'; import 'package:model/extensions/list_presentation_email_extension.dart'; import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; import 'package:model/user/user_profile.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/base/mixin/date_range_picker_mixin.dart'; import 'package:tmail_ui_user/features/contact/presentation/model/contact_arguments.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; +import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/model/recent_search.dart'; @@ -44,6 +49,7 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/domain/usecases/save_re import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/email_receive_time_type.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/quick_search_filter.dart'; +import 'package:tmail_ui_user/features/manage_account/presentation/extensions/datetime_extension.dart'; import 'package:tmail_ui_user/features/search/email/domain/state/refresh_changes_search_email_state.dart'; import 'package:tmail_ui_user/features/search/email/domain/usecases/refresh_changes_search_email_interactor.dart'; import 'package:tmail_ui_user/features/search/email/presentation/model/search_more_state.dart'; @@ -51,6 +57,7 @@ import 'package:tmail_ui_user/features/search/email/presentation/model/simple_se import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_multiple_email_read_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/mark_as_star_multiple_email_state.dart'; @@ -63,10 +70,12 @@ import 'package:tmail_ui_user/features/thread/presentation/extensions/list_prese import 'package:tmail_ui_user/features/thread/presentation/mixin/email_action_controller.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class SearchEmailController extends BaseController - with EmailActionController { + with EmailActionController, + DateRangePickerMixin { final QuickSearchEmailInteractor _quickSearchEmailInteractor; final SaveRecentSearchInteractor _saveRecentSearchInteractor; @@ -84,7 +93,7 @@ class SearchEmailController extends BaseController final listSuggestionSearch = RxList(); final simpleSearchFilter = Rx(SimpleSearchFilter()); final searchIsRunning = RxBool(false); - final emailReceiveTimeType = Rxn(); + final emailReceiveTimeType = EmailReceiveTimeType.allTime.obs; final selectionMode = Rx(SelectMode.INACTIVE); late Debouncer _deBouncerTime; @@ -131,32 +140,28 @@ class SearchEmailController extends BaseController } @override - void onData(Either newState) { - super.onData(newState); - newState.fold( - (failure) { - if (failure is SearchEmailFailure) { - _searchEmailsFailure(failure); - } else if (failure is SearchMoreEmailFailure) { - _searchMoreEmailsFailure(failure); - } - }, - (success) { - if (success is SearchEmailSuccess) { - _searchEmailsSuccess(success); - } else if (success is SearchingMoreState) { - searchMoreState = SearchMoreState.waiting; - } else if (success is SearchMoreEmailSuccess) { - _searchMoreEmailsSuccess(success); - } else if (success is RefreshChangesSearchEmailSuccess) { - _refreshChangesSearchEmailsSuccess(success); - } - } - ); + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is SearchEmailSuccess) { + _searchEmailsSuccess(success); + } else if (success is SearchingMoreState) { + searchMoreState = SearchMoreState.waiting; + } else if (success is SearchMoreEmailSuccess) { + _searchMoreEmailsSuccess(success); + } else if (success is RefreshChangesSearchEmailSuccess) { + _refreshChangesSearchEmailsSuccess(success); + } } @override - void onDone() {} + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is SearchEmailFailure) { + _searchEmailsFailure(failure); + } else if (failure is SearchMoreEmailFailure) { + _searchMoreEmailsFailure(failure); + } + } void _initializeDebounceTimeTextSearchChange() { _deBouncerTime = Debouncer( @@ -169,8 +174,8 @@ class SearchEmailController extends BaseController textOption: value.isNotEmpty ? Some(SearchQuery(value)) : const None(), beforeOption: const None() ); - if (value.isNotEmpty && accountId != null) { - listSuggestionSearch.value = await quickSearchEmails(accountId: accountId!); + if (value.isNotEmpty && session != null && accountId != null) { + listSuggestionSearch.value = await quickSearchEmails(session: session!, accountId: accountId!); } else { listSuggestionSearch.clear(); } @@ -190,43 +195,43 @@ class SearchEmailController extends BaseController void _initWorkerListener() { dashBoardViewStateWorker = ever(mailboxDashBoardController.viewState, (viewState) { - if (viewState is Either) { - viewState.map((success) { - if (success is MarkAsEmailReadSuccess || - success is MoveToMailboxSuccess || - success is MarkAsStarEmailSuccess || - success is DeleteEmailPermanentlySuccess || - success is MarkAsMultipleEmailReadAllSuccess || - success is MarkAsMultipleEmailReadHasSomeEmailFailure || - success is MarkAsStarMultipleEmailAllSuccess || - success is MarkAsStarMultipleEmailHasSomeEmailFailure || - success is MoveMultipleEmailToMailboxAllSuccess || - success is MoveMultipleEmailToMailboxHasSomeEmailFailure || - success is EmptyTrashFolderSuccess || - success is DeleteMultipleEmailsPermanentlyAllSuccess || - success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure - ) { - _refreshEmailChanges(); - } - }); - } + viewState.map((success) { + if (success is MarkAsEmailReadSuccess || + success is MoveToMailboxSuccess || + success is MarkAsStarEmailSuccess || + success is DeleteEmailPermanentlySuccess || + success is MarkAsMultipleEmailReadAllSuccess || + success is MarkAsMultipleEmailReadHasSomeEmailFailure || + success is MarkAsStarMultipleEmailAllSuccess || + success is MarkAsStarMultipleEmailHasSomeEmailFailure || + success is MoveMultipleEmailToMailboxAllSuccess || + success is MoveMultipleEmailToMailboxHasSomeEmailFailure || + success is EmptyTrashFolderSuccess || + success is EmptySpamFolderSuccess || + success is DeleteMultipleEmailsPermanentlyAllSuccess || + success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure + ) { + _refreshEmailChanges(); + } + }); }); } void _refreshEmailChanges() { - if (searchIsRunning.isTrue && accountId != null) { + if (searchIsRunning.isTrue && session != null && accountId != null) { final limit = listResultSearch.isNotEmpty ? UnsignedInt(listResultSearch.length) : ThreadConstants.defaultLimit; _updateSimpleSearchFilter(beforeOption: const None()); consumeState(_refreshChangesSearchEmailInteractor.execute( + session!, accountId!, limit: limit, sort: {} ..add(EmailComparator(EmailComparatorProperty.receivedAt) ..setIsAscending(false)), filter: simpleSearchFilter.value.mappingToEmailFilterCondition(), - properties: ThreadConstants.propertiesDefault, + properties: EmailUtils.getPropertiesForEmailGetMethod(session!, accountId!), )); } } @@ -258,15 +263,20 @@ class SearchEmailController extends BaseController : [])); } - Future> quickSearchEmails({required AccountId accountId}) async { + Future> quickSearchEmails({ + required AccountId accountId, + required Session session, + }) async { return _quickSearchEmailInteractor - .execute(accountId, - limit: UnsignedInt(5), + .execute( + session, + accountId, + limit: UnsignedInt(5), sort: {}..add( - EmailComparator(EmailComparatorProperty.receivedAt) - ..setIsAscending(false)), - filter: simpleSearchFilter.value.mappingToEmailFilterCondition(), - properties: ThreadConstants.propertiesQuickSearch) + EmailComparator(EmailComparatorProperty.receivedAt) + ..setIsAscending(false)), + filter: simpleSearchFilter.value.mappingToEmailFilterCondition(), + properties: ThreadConstants.propertiesQuickSearch) .then((result) => result.fold( (failure) => [], (success) => success is QuickSearchEmailSuccess @@ -279,21 +289,22 @@ class SearchEmailController extends BaseController } void _searchEmailAction(BuildContext context) { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); - if (accountId != null) { + if (session != null && accountId != null) { canSearchMore = true; searchIsRunning.value = true; cancelSelectionMode(context); consumeState(_searchEmailInteractor.execute( + session!, accountId!, limit: ThreadConstants.defaultLimit, sort: {} ..add(EmailComparator(EmailComparatorProperty.receivedAt) ..setIsAscending(false)), filter: simpleSearchFilter.value.mappingToEmailFilterCondition(), - properties: ThreadConstants.propertiesDefault, + properties: EmailUtils.getPropertiesForEmailGetMethod(session!, accountId!), )); } } @@ -326,19 +337,20 @@ class SearchEmailController extends BaseController } void searchMoreEmailsAction() { - if (canSearchMore && accountId != null) { + if (canSearchMore && session != null && accountId != null) { final lastEmail = listResultSearch.last; _updateSimpleSearchFilter(beforeOption: optionOf(lastEmail.receivedAt)); consumeState(_searchMoreEmailInteractor.execute( - accountId!, - limit: ThreadConstants.defaultLimit, - sort: {} - ..add(EmailComparator(EmailComparatorProperty.receivedAt) - ..setIsAscending(false)), - filter: simpleSearchFilter.value.mappingToEmailFilterCondition(), - properties: ThreadConstants.propertiesDefault, - lastEmailId: lastEmail.id + session!, + accountId!, + limit: ThreadConstants.defaultLimit, + sort: {} + ..add(EmailComparator(EmailComparatorProperty.receivedAt) + ..setIsAscending(false)), + filter: simpleSearchFilter.value.mappingToEmailFilterCondition(), + properties: EmailUtils.getPropertiesForEmailGetMethod(session!, accountId!), + lastEmailId: lastEmail.id )); } } @@ -401,10 +413,7 @@ class SearchEmailController extends BaseController case QuickSearchFilter.hasAttachment: return simpleSearchFilter.value.hasAttachment == true; case QuickSearchFilter.last7Days: - if (emailReceiveTimeType.value != null) { - return true; - } - return simpleSearchFilter.value.emailReceiveTimeType == EmailReceiveTimeType.last7Days; + return true; default: return false; } @@ -420,86 +429,79 @@ class SearchEmailController extends BaseController ); break; case QuickSearchFilter.last7Days: - if (filterSelected) { - _setEmailReceiveTimeType(null); - _updateSimpleSearchFilter( - emailReceiveTimeTypeOption: const Some(EmailReceiveTimeType.allTime), - beforeOption: const None() - ); - } else { - _setEmailReceiveTimeType(EmailReceiveTimeType.last7Days); - _updateSimpleSearchFilter( - emailReceiveTimeTypeOption: const Some(EmailReceiveTimeType.last7Days), - beforeOption: const None() - ); - } + _updateSimpleSearchFilter( + emailReceiveTimeTypeOption: optionOf(emailReceiveTimeType.value), + beforeOption: const None() + ); break; default: break; } } - void _setEmailReceiveTimeType(EmailReceiveTimeType? receiveTimeType) { + void _setEmailReceiveTimeType(EmailReceiveTimeType receiveTimeType) { emailReceiveTimeType.value = receiveTimeType; } - void selectReceiveTimeQuickSearchFilter(BuildContext context, EmailReceiveTimeType? emailReceiveTimeType) { + void selectReceiveTimeQuickSearchFilter(BuildContext context, EmailReceiveTimeType emailReceiveTimeType) { popBack(); - if (emailReceiveTimeType != null) { - _updateSimpleSearchFilter( - emailReceiveTimeTypeOption: Some(emailReceiveTimeType), - textOption: searchQuery == null - ? Some(SearchQuery.initial()) - : optionOf(simpleSearchFilter.value.text), - beforeOption: const None() + if (emailReceiveTimeType == EmailReceiveTimeType.customRange) { + showMultipleViewDateRangePicker( + context, + simpleSearchFilter.value.startDate?.value.toLocal(), + simpleSearchFilter.value.endDate?.value.toLocal(), + onCallbackAction: (newStartDate, newEndDate) { + _updateSimpleSearchFilter( + emailReceiveTimeTypeOption: Some(emailReceiveTimeType), + textOption: searchQuery == null + ? Some(SearchQuery.initial()) + : optionOf(simpleSearchFilter.value.text), + beforeOption: const None(), + startDateOption: optionOf(newStartDate?.toUTCDate()), + endDateOption: optionOf(newEndDate?.toUTCDate()), + ); + + _setEmailReceiveTimeType(emailReceiveTimeType); + _searchEmailAction(context); + } ); } else { _updateSimpleSearchFilter( - emailReceiveTimeTypeOption: const Some(EmailReceiveTimeType.allTime), + emailReceiveTimeTypeOption: Some(emailReceiveTimeType), textOption: searchQuery == null ? Some(SearchQuery.initial()) : optionOf(simpleSearchFilter.value.text), - beforeOption: const None() + beforeOption: const None(), + startDateOption: const None(), + endDateOption: const None() ); + + _setEmailReceiveTimeType(emailReceiveTimeType); + _searchEmailAction(context); } - _setEmailReceiveTimeType(emailReceiveTimeType); - _searchEmailAction(context); } void selectMailboxForSearchFilter(BuildContext context, PresentationMailbox? mailbox) async { final arguments = DestinationPickerArguments( - mailboxDashBoardController.accountId.value!, - MailboxActions.select, - mailboxDashBoardController.sessionCurrent, - mailboxIdSelected: mailbox?.id); - - if (BuildUtils.isWeb) { - showDialogDestinationPicker( - context: context, - arguments: arguments, - onSelectedMailbox: (destinationMailbox) { - final mailboxSelected = destinationMailbox == PresentationMailbox.unifiedMailbox ? null : destinationMailbox; - if (mailboxSelected != null && mailbox?.id != mailboxSelected.id) { - _updateSimpleSearchFilter( - mailboxOption: Some(mailboxSelected), - beforeOption: const None() - ); - _searchEmailAction(context); - } - }); - } else { - final destinationMailbox = await push( - AppRoutes.destinationPicker, - arguments: arguments); + mailboxDashBoardController.accountId.value!, + MailboxActions.select, + mailboxDashBoardController.sessionCurrent, + mailboxIdSelected: mailbox?.id); + + final destinationMailbox = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments) + : await push(AppRoutes.destinationPicker, arguments: arguments); + + if (destinationMailbox is PresentationMailbox) { + final mailboxSelected = destinationMailbox == PresentationMailbox.unifiedMailbox ? null : destinationMailbox; + if (mailboxSelected != null && mailbox?.id != mailboxSelected.id) { + _updateSimpleSearchFilter( + mailboxOption: Some(mailboxSelected), + beforeOption: const None() + ); - if (destinationMailbox is PresentationMailbox) { - final mailboxSelected = destinationMailbox == PresentationMailbox.unifiedMailbox ? null : destinationMailbox; - if (mailboxSelected != null && mailbox?.id != mailboxSelected.id) { - _updateSimpleSearchFilter( - mailboxOption: Some(mailboxSelected), - beforeOption: const None() - ); + if (context.mounted) { _searchEmailAction(context); } } @@ -514,29 +516,14 @@ class SearchEmailController extends BaseController final listContactSelected = simpleSearchFilter.value.getContactApplied(prefixEmailAddress); final arguments = ContactArguments(accountId!, session!, listContactSelected); - if (BuildUtils.isWeb) { - showDialogContactView( - context: context, - arguments: arguments, - onSelectedContact: (newContact) { - _dispatchApplyContactAction( - context, - listContactSelected, - prefixEmailAddress, - newContact); - }); - } else { - final newContact = await push( - AppRoutes.contact, - arguments: arguments); + final newContact = await push(AppRoutes.contact, arguments: arguments); - if (newContact is EmailAddress) { - _dispatchApplyContactAction( - context, - listContactSelected, - prefixEmailAddress, - newContact); - } + if (newContact is EmailAddress && context.mounted) { + _dispatchApplyContactAction( + context, + listContactSelected, + prefixEmailAddress, + newContact); } } } @@ -609,6 +596,8 @@ class SearchEmailController extends BaseController Option? emailReceiveTimeTypeOption, Option? hasAttachmentOption, Option? beforeOption, + Option? startDateOption, + Option? endDateOption }) { simpleSearchFilter.value = simpleSearchFilter.value.copyWith( fromOption: fromOption, @@ -618,6 +607,8 @@ class SearchEmailController extends BaseController emailReceiveTimeTypeOption: emailReceiveTimeTypeOption, hasAttachmentOption: hasAttachmentOption, beforeOption: beforeOption, + startDateOption: startDateOption, + endDateOption: endDateOption ); simpleSearchFilter.refresh(); } @@ -626,6 +617,14 @@ class SearchEmailController extends BaseController _deBouncerTime.value = text; } + void onTextSearchSubmitted(BuildContext context, String text) { + final query = text.trim(); + if (query.isNotEmpty) { + saveRecentSearch(RecentSearch.now(query)); + submitSearchAction(context, query); + } + } + void setTextInputSearchForm(String value) { textInputSearchController.text = value; } @@ -650,7 +649,7 @@ class SearchEmailController extends BaseController log('SearchEmailController::closeSearchView(): '); clearAllTextInputSearchForm(); clearAllResultSearch(); - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); mailboxDashBoardController.searchController.disableAllSearchEmail(); mailboxDashBoardController.dispatchMailboxUIAction(SelectMailboxDefaultAction()); mailboxDashBoardController.dispatchRoute(DashboardRoutes.thread); @@ -666,7 +665,7 @@ class SearchEmailController extends BaseController switch(actionType) { case EmailActionType.preview: if (mailboxContain?.isDrafts == true) { - editEmail(selectedEmail); + editDraftEmail(selectedEmail); } else { previewEmail(selectedEmail); } @@ -675,10 +674,10 @@ class SearchEmailController extends BaseController selectEmail(context, selectedEmail); break; case EmailActionType.markAsRead: - markAsEmailRead(selectedEmail, ReadActions.markAsRead); + markAsEmailRead(selectedEmail, ReadActions.markAsRead, MarkReadAction.tap); break; case EmailActionType.markAsUnread: - markAsEmailRead(selectedEmail, ReadActions.markAsUnread); + markAsEmailRead(selectedEmail, ReadActions.markAsUnread, MarkReadAction.tap); break; case EmailActionType.markAsStarred: markAsStarEmail(selectedEmail, MarkStarAction.markStar); diff --git a/lib/features/search/email/presentation/search_email_view.dart b/lib/features/search/email/presentation/search_email_view.dart index de2a643ba5..d3f33fac98 100644 --- a/lib/features/search/email/presentation/search_email_view.dart +++ b/lib/features/search/email/presentation/search_email_view.dart @@ -3,10 +3,10 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; -import 'package:core/presentation/views/background/background_widget_builder.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; import 'package:core/presentation/views/text/text_field_builder.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -27,6 +27,7 @@ import 'package:tmail_ui_user/features/search/email/presentation/widgets/email_r import 'package:tmail_ui_user/features/search/email/presentation/widgets/email_receive_time_cupertino_action_sheet_action_builder.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_more_email_state.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/empty_emails_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart' if (dart.library.html) 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_web_builder.dart'; @@ -108,37 +109,30 @@ class SearchEmailView extends GetWidget return Row( children: [ buildIconWeb( - icon: SvgPicture.asset(_imagePaths.icBack, - width: 18, - height: 18, - color: AppColor.colorTextButton, - fit: BoxFit.fill), + icon: SvgPicture.asset( + DirectionUtils.isDirectionRTLByLanguage(context) ? _imagePaths.icCollapseFolder : _imagePaths.icBack, + colorFilter: AppColor.colorTextButton.asFilter(), + fit: BoxFit.fill + ), tooltip: AppLocalizations.of(context).back, onTap: () => controller.closeSearchView(context) ), - Expanded(child: (TextFieldBuilder() - ..onChange(controller.onTextSearchChange) - ..textInputAction(TextInputAction.search) - ..addController(controller.textInputSearchController) - ..addFocusNode(controller.textInputSearchFocus) - ..textStyle(const TextStyle(color: Colors.black, fontSize: 16)) - ..keyboardType(TextInputType.text) - ..onSubmitted((value) { - final query = value.trim(); - if (query.isNotEmpty) { - controller.saveRecentSearch(RecentSearch.now(query)); - controller.submitSearchAction(context, query); - } - }) - ..maxLines(1) - ..textDecoration(InputDecoration( - contentPadding: const EdgeInsets.all(12), - hintText: AppLocalizations.of(context).search_emails, - hintStyle: const TextStyle( - color: AppColor.loginTextFieldHintColor, - fontSize: 16), - border: InputBorder.none))) - .build()), + Expanded(child: TextFieldBuilder( + onTextChange: controller.onTextSearchChange, + textInputAction: TextInputAction.search, + controller: controller.textInputSearchController, + focusNode: controller.textInputSearchFocus, + maxLines: 1, + textDirection: DirectionUtils.getDirectionByLanguage(context), + textStyle: const TextStyle(color: Colors.black, fontSize: 16), + keyboardType: TextInputType.text, + onTextSubmitted: (text) => controller.onTextSearchSubmitted(context, text), + decoration: InputDecoration( + contentPadding: const EdgeInsets.all(12), + hintText: AppLocalizations.of(context).search_emails, + hintStyle: const TextStyle(color: AppColor.loginTextFieldHintColor, fontSize: 16), + border: InputBorder.none), + )), Obx(() { if (controller.currentSearchText.isNotEmpty) { return @@ -220,30 +214,35 @@ class SearchEmailView extends GetWidget child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), - color: filter.getBackgroundColor(quickSearchFilterSelected: filterSelected)), + color: filter.getBackgroundColor(isFilterSelected: filterSelected)), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ SvgPicture.asset( - filter.getIcon(_imagePaths, quickSearchFilterSelected: filterSelected), + filter.getIcon(_imagePaths, isFilterSelected: filterSelected), width: 16, height: 16, fit: BoxFit.fill), const SizedBox(width: 4), Text( - filter.getTitle(context, receiveTimeType: controller.emailReceiveTimeType.value), + filter.getTitle( + context, + receiveTimeType: controller.emailReceiveTimeType.value, + startDate: controller.simpleSearchFilter.value.startDate?.value.toLocal(), + endDate: controller.simpleSearchFilter.value.endDate?.value.toLocal() + ), maxLines: 1, overflow: CommonTextStyle.defaultTextOverFlow, softWrap: CommonTextStyle.defaultSoftWrap, - style: filter.getTextStyle(quickSearchFilterSelected: filterSelected), + style: filter.getTextStyle(isFilterSelected: filterSelected), ), if (filter == QuickSearchFilter.last7Days) ... [ const SizedBox(width: 4), SvgPicture.asset( _imagePaths.icChevronDownOutline, - color: filterSelected - ? AppColor.primaryColor - : AppColor.colorDefaultRichTextButton, + colorFilter: filterSelected + ? AppColor.primaryColor.asFilter() + : AppColor.colorDefaultRichTextButton.asFilter(), fit: BoxFit.fill), ] ]) @@ -256,22 +255,23 @@ class SearchEmailView extends GetWidget List _popupMenuEmailReceiveTimeType( BuildContext context, EmailReceiveTimeType? receiveTimeSelected, - Function(EmailReceiveTimeType?)? onCallBack + Function(EmailReceiveTimeType)? onCallBack ) { return EmailReceiveTimeType.values - .map((timeType) => PopupMenuItem( + .map((timeType) => PopupMenuItem( padding: EdgeInsets.zero, child: EmailReceiveTimeActionTileWidget( - receiveTimeSelected: receiveTimeSelected, - receiveTimeType: timeType, - onCallBack: onCallBack))) - .toList(); + receiveTimeSelected: receiveTimeSelected, + receiveTimeType: timeType, + onCallBack: onCallBack + ))) + .toList(); } List _emailReceiveTimeCupertinoActionTile( BuildContext context, EmailReceiveTimeType? receiveTimeSelected, - Function(EmailReceiveTimeType?)? onCallBack + Function(EmailReceiveTimeType)? onCallBack ) { return EmailReceiveTimeType.values .map((timeType) => (EmailReceiveTimeCupertinoActionSheetActionBuilder( @@ -289,7 +289,7 @@ class SearchEmailView extends GetWidget width: 20, height: 20, fit: BoxFit.fill)) - ..onActionClick((timeType) => onCallBack?.call(timeType == receiveTimeSelected ? null : timeType))) + ..onActionClick((timeType) => onCallBack?.call(timeType))) .build()) .toList(); } @@ -392,9 +392,9 @@ class SearchEmailView extends GetWidget return Obx(() => controller.viewState.value.fold( (failure) => const SizedBox.shrink(), (success) => success is! SearchingState - ? BackgroundWidgetBuilder( - AppLocalizations.of(context).no_emails_matching_your_search, - controller.responsiveUtils, + ? EmptyEmailsWidget( + key: const Key('empty_search_email_view'), + title: AppLocalizations.of(context).no_emails_matching_your_search, iconSVG: _imagePaths.icEmptyEmail ) : const SizedBox.shrink()) @@ -440,7 +440,7 @@ class SearchEmailView extends GetWidget } double? _getItemExtent(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return _responsiveUtils.isDesktop(context) ? 52 : 95; } else { return null; @@ -469,8 +469,11 @@ class SearchEmailView extends GetWidget return (EmailActionCupertinoActionSheetActionBuilder( const Key('mark_as_spam_or_un_spam_action'), SvgPicture.asset( - mailboxContain?.isSpam == true ? _imagePaths.icNotSpam : _imagePaths.icSpam, - width: 28, height: 28, fit: BoxFit.fill, color: AppColor.colorTextButton), + mailboxContain?.isSpam == true ? _imagePaths.icNotSpam : _imagePaths.icSpam, + width: 28, + height: 28, + fit: BoxFit.fill, + colorFilter: AppColor.colorTextButton.asFilter()), mailboxContain?.isSpam == true ? AppLocalizations.of(context).remove_from_spam : AppLocalizations.of(context).mark_as_spam, @@ -535,8 +538,8 @@ class SearchEmailView extends GetWidget const SizedBox(width: 4), Text( filterSelected - ? controller.simpleSearchFilter.value.mailboxName - : AppLocalizations.of(context).mailbox, + ? controller.simpleSearchFilter.value.getMailboxName(context) + : AppLocalizations.of(context).mailbox, maxLines: 1, overflow: CommonTextStyle.defaultTextOverFlow, softWrap: CommonTextStyle.defaultSoftWrap, @@ -553,9 +556,9 @@ class SearchEmailView extends GetWidget const SizedBox(width: 4), SvgPicture.asset( _imagePaths.icChevronDownOutline, - color: filterSelected - ? AppColor.primaryColor - : AppColor.colorDefaultRichTextButton, + colorFilter: filterSelected + ? AppColor.primaryColor.asFilter() + : AppColor.colorDefaultRichTextButton.asFilter(), fit: BoxFit.fill), ]) ), @@ -607,9 +610,9 @@ class SearchEmailView extends GetWidget const SizedBox(width: 4), SvgPicture.asset( _imagePaths.icChevronDownOutline, - color: filterSelected - ? AppColor.primaryColor - : AppColor.colorDefaultRichTextButton, + colorFilter: filterSelected + ? AppColor.primaryColor.asFilter() + : AppColor.colorDefaultRichTextButton.asFilter(), fit: BoxFit.fill), ]) ), diff --git a/lib/features/search/email/presentation/utils/search_email_utils.dart b/lib/features/search/email/presentation/utils/search_email_utils.dart index 9dad121616..ed10efeee4 100644 --- a/lib/features/search/email/presentation/utils/search_email_utils.dart +++ b/lib/features/search/email/presentation/utils/search_email_utils.dart @@ -1,11 +1,11 @@ import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; class SearchEmailUtils { static EdgeInsets getPaddingAppBar(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return const EdgeInsets.symmetric(horizontal: 16); } else { if (responsiveUtils.isScreenWithShortestSide(context)) { @@ -17,7 +17,7 @@ class SearchEmailUtils { } static EdgeInsets getPaddingSearchSuggestionList(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { + if (PlatformInfo.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { return const EdgeInsets.symmetric(horizontal: 32, vertical: 12); } else { return const EdgeInsets.symmetric(horizontal: 16, vertical: 12); @@ -25,7 +25,7 @@ class SearchEmailUtils { } static EdgeInsets getPaddingShowAllResultButton(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { + if (PlatformInfo.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { return const EdgeInsets.symmetric(horizontal: 32, vertical: 12); } else { return const EdgeInsets.symmetric(horizontal: 16, vertical: 12); @@ -33,7 +33,7 @@ class SearchEmailUtils { } static EdgeInsets getPaddingSearchRecentTitle(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { + if (PlatformInfo.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { return const EdgeInsets.symmetric(horizontal: 32, vertical: 8); } else { return const EdgeInsets.symmetric(horizontal: 16, vertical: 8); @@ -41,7 +41,7 @@ class SearchEmailUtils { } static EdgeInsets getPaddingListRecentSearch(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { + if (PlatformInfo.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { return const EdgeInsets.symmetric(horizontal: 32, vertical: 12); } else { return const EdgeInsets.symmetric(horizontal: 16, vertical: 12); @@ -49,7 +49,7 @@ class SearchEmailUtils { } static EdgeInsets getPaddingSearchFilterButton(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { + if (PlatformInfo.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { return const EdgeInsets.all(12); } else { return const EdgeInsets.symmetric(vertical: 12, horizontal: 16); @@ -57,7 +57,7 @@ class SearchEmailUtils { } static EdgeInsets getMarginSearchFilterButton(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { + if (PlatformInfo.isWeb || !responsiveUtils.isScreenWithShortestSide(context)) { return const EdgeInsets.symmetric(horizontal: 20); } else { return EdgeInsets.zero; @@ -65,7 +65,7 @@ class SearchEmailUtils { } static EdgeInsets getPaddingSearchResultList(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return const EdgeInsets.only(left: 10); } else { if (responsiveUtils.isScreenWithShortestSide(context)) { @@ -77,7 +77,7 @@ class SearchEmailUtils { } static EdgeInsets getPaddingDividerSearchResultList(BuildContext context, ResponsiveUtils responsiveUtils) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return const EdgeInsets.symmetric(horizontal: 16); } else { if (responsiveUtils.isScreenWithShortestSide(context)) { diff --git a/lib/features/search/email/presentation/widgets/app_bar_selection_mode.dart b/lib/features/search/email/presentation/widgets/app_bar_selection_mode.dart index 83960862a1..4da6f2f578 100644 --- a/lib/features/search/email/presentation/widgets/app_bar_selection_mode.dart +++ b/lib/features/search/email/presentation/widgets/app_bar_selection_mode.dart @@ -38,9 +38,10 @@ class AppBarSelectionMode extends StatelessWidget { return Row(children: [ buildIconWeb( - icon: SvgPicture.asset(_imagePaths.icCloseComposer, - color: AppColor.colorTextButton, - fit: BoxFit.fill), + icon: SvgPicture.asset( + _imagePaths.icClose, + colorFilter: AppColor.colorTextButton.asFilter(), + fit: BoxFit.fill), minSize: 25, iconSize: 25, iconPadding: const EdgeInsets.all(5), @@ -128,11 +129,11 @@ class AppBarSelectionMode extends StatelessWidget { splashRadius: 15, icon: SvgPicture.asset( canDeletePermanently - ? _imagePaths.icDeleteComposer - : _imagePaths.icDelete, - color: canDeletePermanently - ? AppColor.colorDeletePermanentlyButton - : AppColor.primaryColor, + ? _imagePaths.icDeleteComposer + : _imagePaths.icDelete, + colorFilter: canDeletePermanently + ? AppColor.colorDeletePermanentlyButton.asFilter() + : AppColor.primaryColor.asFilter(), width: 20, height: 20, fit: BoxFit.fill), diff --git a/lib/features/search/email/presentation/widgets/email_receive_time_action_tile_widget.dart b/lib/features/search/email/presentation/widgets/email_receive_time_action_tile_widget.dart index bd310784f8..7e126dcf7d 100644 --- a/lib/features/search/email/presentation/widgets/email_receive_time_action_tile_widget.dart +++ b/lib/features/search/email/presentation/widgets/email_receive_time_action_tile_widget.dart @@ -9,7 +9,7 @@ class EmailReceiveTimeActionTileWidget extends StatelessWidget { final EmailReceiveTimeType? receiveTimeSelected; final EmailReceiveTimeType receiveTimeType; - final Function(EmailReceiveTimeType?)? onCallBack; + final Function(EmailReceiveTimeType)? onCallBack; const EmailReceiveTimeActionTileWidget({ Key? key, @@ -23,7 +23,7 @@ class EmailReceiveTimeActionTileWidget extends StatelessWidget { final imagePaths = Get.find(); return InkWell( - onTap: () => onCallBack?.call(receiveTimeType == receiveTimeSelected ? null : receiveTimeType), + onTap: () => onCallBack?.call(receiveTimeType), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), child: SizedBox( diff --git a/lib/features/search/email/presentation/widgets/email_receive_time_cupertino_action_sheet_action_builder.dart b/lib/features/search/email/presentation/widgets/email_receive_time_cupertino_action_sheet_action_builder.dart index abbedcce62..655bd363d4 100644 --- a/lib/features/search/email/presentation/widgets/email_receive_time_cupertino_action_sheet_action_builder.dart +++ b/lib/features/search/email/presentation/widgets/email_receive_time_cupertino_action_sheet_action_builder.dart @@ -1,6 +1,6 @@ import 'package:core/presentation/views/bottom_popup/cupertino_action_sheet_no_icon_builder.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; @@ -32,7 +32,7 @@ class EmailReceiveTimeCupertinoActionSheetActionBuilder return Container( color: bgColor ?? Colors.white, child: MouseRegion( - cursor: BuildUtils.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( key: key, child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/features/search/mailbox/presentation/search_mailbox_bindings.dart b/lib/features/search/mailbox/presentation/search_mailbox_bindings.dart index a3a89a4523..92fa3dfb94 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_bindings.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_bindings.dart @@ -2,7 +2,7 @@ import 'package:core/data/model/source_type/data_source_type.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/mailbox_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/mailbox_cache_datasource_impl.dart'; @@ -13,6 +13,7 @@ import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_api.dart'; import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_isolate_worker.dart'; import 'package:tmail_ui_user/features/mailbox/data/repository/mailbox_repository_impl.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_mailbox_interactor.dart'; @@ -38,6 +39,7 @@ class SearchMailboxBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), Get.find(), Get.find(), Get.find(), diff --git a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart index 0b1caeae02..d99b33d7ca 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_controller.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_controller.dart @@ -4,14 +4,17 @@ import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/presentation/utils/keyboard_utils.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; -import 'package:dartz/dartz.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/presentation_email_extension.dart'; @@ -20,6 +23,7 @@ import 'package:tmail_ui_user/features/base/base_mailbox_controller.dart'; import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:tmail_ui_user/features/base/mixin/mailbox_action_handler_mixin.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_subscribe_action_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/mailbox_subscribe_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/move_mailbox_request.dart'; @@ -27,6 +31,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/model/rename_mailbox_reque import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_multiple_mailbox_request.dart'; import 'package:tmail_ui_user/features/mailbox/domain/model/subscribe_request.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/state/create_new_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/delete_multiple_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/get_all_mailboxes_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; @@ -36,6 +41,7 @@ import 'package:tmail_ui_user/features/mailbox/domain/state/rename_mailbox_state import 'package:tmail_ui_user/features/mailbox/domain/state/search_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/subscribe_mailbox_state.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/subscribe_multiple_mailbox_state.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/usecases/create_new_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/delete_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/get_all_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/move_mailbox_interactor.dart'; @@ -45,17 +51,23 @@ import 'package:tmail_ui_user/features/mailbox/domain/usecases/search_mailbox_in import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/domain/usecases/subscribe_multiple_mailbox_interactor.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/action/mailbox_ui_action.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_actions.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/model/mailbox_tree_builder.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_utils.dart'; import 'package:tmail_ui_user/features/mailbox_creator/domain/usecases/verify_name_interactor.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/mailbox_creator_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/presentation/model/new_mailbox_arguments.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:uuid/uuid.dart'; class SearchMailboxController extends BaseMailboxController with MailboxActionHandlerMixin { @@ -65,11 +77,13 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa final DeleteMultipleMailboxInteractor _deleteMultipleMailboxInteractor; final SubscribeMailboxInteractor _subscribeMailboxInteractor; final SubscribeMultipleMailboxInteractor _subscribeMultipleMailboxInteractor; + final CreateNewMailboxInteractor _createNewMailboxInteractor; final dashboardController = Get.find(); final responsiveUtils = Get.find(); final imagePaths = Get.find(); final _appToast = Get.find(); + final _uuid = Get.find(); final currentSearchQuery = RxString(''); final listMailboxSearched = RxList(); @@ -87,6 +101,7 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa this._deleteMultipleMailboxInteractor, this._subscribeMailboxInteractor, this._subscribeMultipleMailboxInteractor, + this._createNewMailboxInteractor, TreeBuilder treeBuilder, VerifyNameInteractor verifyNameInteractor, GetAllMailboxInteractor getAllMailboxInteractor, @@ -106,33 +121,32 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa } @override - void onDone() { - viewState.value.fold(_handleFailureViewState, _handleSuccessViewState); - } - - @override - void onData(Either newState) { - super.onData(newState); - newState.fold((failure) => null, (success) async { - if (success is GetAllMailboxSuccess) { - currentMailboxState = success.currentMailboxState; - buildTree(success.mailboxList); - } else if (success is RefreshChangesAllMailboxSuccess) { - currentMailboxState = success.currentMailboxState; - await refreshTree(success.mailboxList); - searchMailboxAction(); - } - }); - } - - void _handleFailureViewState(Failure failure) { + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); if (failure is SearchMailboxFailure) { _handleSearchMailboxFailure(failure); + } else if (failure is CreateNewMailboxFailure) { + _createNewMailboxFailure(failure); } } - void _handleSuccessViewState(Success success) { - if (success is SearchMailboxSuccess) { + @override + void handleSuccessViewState(Success success) async { + super.handleSuccessViewState(success); + if (success is GetAllMailboxSuccess) { + currentMailboxState = success.currentMailboxState; + await buildTree(success.mailboxList); + if (currentContext != null) { + await syncAllMailboxWithDisplayName(currentContext!); + } + } else if (success is RefreshChangesAllMailboxSuccess) { + currentMailboxState = success.currentMailboxState; + await refreshTree(success.mailboxList); + if (currentContext != null) { + await syncAllMailboxWithDisplayName(currentContext!); + } + searchMailboxAction(); + } else if (success is SearchMailboxSuccess) { _handleSearchMailboxSuccess(success); } else if (success is MarkAsMailboxReadAllSuccess) { _refreshMailboxChanges(mailboxState: success.currentMailboxState); @@ -152,6 +166,8 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _handleSubscribeMultipleMailboxAllSuccess(success); } else if (success is SubscribeMultipleMailboxHasSomeSuccess) { _handleSubscribeMultipleMailboxHasSomeSuccess(success); + } else if (success is CreateNewMailboxSuccess) { + _createNewMailboxSuccess(success); } } @@ -198,7 +214,7 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa } void handleSearchButtonPressed(BuildContext context) { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); searchMailboxAction(); } @@ -215,12 +231,19 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa _deBouncerTime.value = text; } + void onTextSearchSubmitted(BuildContext context, String text) { + final query = text.trim(); + if (query.isNotEmpty) { + submitSearchAction(context, query); + } + } + void setTextInputSearchForm(String value) { textInputSearchController.text = value; } void submitSearchAction(BuildContext context, String query) { - FocusScope.of(context).unfocus(); + KeyboardUtils.hideKeyboard(context); currentSearchQuery.value = query; searchMailboxAction(); } @@ -259,7 +282,7 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa context, mailbox, dashboardController, - onMovingMailboxAction: _invokeMovingMailboxAction + onMovingMailboxAction: (mailboxSelected, destinationMailbox) => _invokeMovingMailboxAction(context, mailboxSelected, destinationMailbox) ); break; case MailboxActions.delete: @@ -285,6 +308,15 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa MailboxSubscribeAction.subscribe ); break; + case MailboxActions.emptyTrash: + emptyTrashAction(context, mailbox, dashboardController); + break; + case MailboxActions.emptySpam: + emptySpamAction(context, mailbox, dashboardController); + break; + case MailboxActions.newSubfolder: + goToCreateNewMailboxView(context, parentMailbox: mailbox); + break; default: break; } @@ -292,18 +324,27 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa void _renameMailboxAction(PresentationMailbox presentationMailbox, MailboxName newMailboxName) { final accountId = dashboardController.accountId.value; - if (accountId != null) { + final session = dashboardController.sessionCurrent; + if (session != null && accountId != null) { consumeState(_renameMailboxInteractor.execute( + session, accountId, RenameMailboxRequest(presentationMailbox.id, newMailboxName) )); } } - void _invokeMovingMailboxAction(PresentationMailbox mailboxSelected, PresentationMailbox? destinationMailbox) { + void _invokeMovingMailboxAction( + BuildContext context, + PresentationMailbox mailboxSelected, + PresentationMailbox? destinationMailbox + ) { final accountId = dashboardController.accountId.value; - if (accountId != null) { + final session = dashboardController.sessionCurrent; + if (session != null && accountId != null) { _handleMovingMailbox( + context, + session, accountId, MoveAction.moving, mailboxSelected, @@ -313,18 +354,21 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa } void _handleMovingMailbox( + BuildContext context, + Session session, AccountId accountId, MoveAction moveAction, PresentationMailbox mailboxSelected, {PresentationMailbox? destinationMailbox} ) { consumeState(_moveMailboxInteractor.execute( + session, accountId, MoveMailboxRequest( mailboxSelected.id, moveAction, destinationMailboxId: destinationMailbox?.id, - destinationMailboxName: destinationMailbox?.name, + destinationMailboxDisplayName: destinationMailbox?.getDisplayName(context), parentId: mailboxSelected.parentId ) )); @@ -332,9 +376,9 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa void _moveMailboxSuccess(MoveMailboxSuccess success) { if (success.moveAction == MoveAction.moving && currentOverlayContext != null && currentContext != null) { - _appToast.showBottomToast( + _appToast.showToastMessage( currentOverlayContext!, - AppLocalizations.of(currentContext!).moved_to_mailbox(success.destinationMailboxName?.name ?? AppLocalizations.of(currentContext!).allMailboxes), + AppLocalizations.of(currentContext!).moved_to_mailbox(success.destinationMailboxDisplayName ?? AppLocalizations.of(currentContext!).allMailboxes), actionName: AppLocalizations.of(currentContext!).undo, onActionClick: () { _undoMovingMailbox(MoveMailboxRequest( @@ -344,17 +388,11 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa parentId: success.destinationMailboxId) ); }, - leadingIcon: SvgPicture.asset( - imagePaths.icFolderMailbox, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill), + leadingSVGIconColor: Colors.white, + leadingSVGIcon: imagePaths.icFolderMailbox, backgroundColor: AppColor.toastSuccessBackgroundColor, textColor: Colors.white, - textActionColor: Colors.white, - actionIcon: SvgPicture.asset(imagePaths.icUndo), - maxWidth: responsiveUtils.getMaxWidthToast(currentContext!) + actionIcon: SvgPicture.asset(imagePaths.icUndo) ); } @@ -363,8 +401,9 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa void _undoMovingMailbox(MoveMailboxRequest newMoveRequest) { final accountId = dashboardController.accountId.value; - if (accountId != null) { - consumeState(_moveMailboxInteractor.execute(accountId, newMoveRequest)); + final session = dashboardController.sessionCurrent; + if (session != null && accountId != null) { + consumeState(_moveMailboxInteractor.execute(session, accountId, newMoveRequest)); } } @@ -396,11 +435,9 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa void _deleteMultipleMailboxSuccess(List listMailboxIdDeleted, jmap.State? currentMailboxState) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( + _appToast.showToastSuccessMessage( currentOverlayContext!, - message: AppLocalizations.of(currentContext!).delete_mailboxes_successfully, - icon: imagePaths.icSelected - ); + AppLocalizations.of(currentContext!).delete_mailboxes_successfully); } if (listMailboxIdDeleted.contains(dashboardController.selectedMailbox.value?.id)) { @@ -413,11 +450,9 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa void _deleteMailboxFailure(DeleteMultipleMailboxFailure failure) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showToastWithIcon( + _appToast.showToastErrorMessage( currentOverlayContext!, - message: AppLocalizations.of(currentContext!).delete_mailboxes_failure, - icon: imagePaths.icDeleteToast - ); + AppLocalizations.of(currentContext!).delete_mailboxes_failure); } } @@ -426,21 +461,23 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa MailboxSubscribeState subscribeState, MailboxSubscribeAction subscribeAction ) { - final _accountId = dashboardController.accountId.value; - if (_accountId != null) { + final accountId = dashboardController.accountId.value; + final session = dashboardController.sessionCurrent; + + if (session != null && accountId != null) { final subscribeRequest = generateSubscribeRequest(mailboxId, subscribeState, subscribeAction); if (subscribeRequest is SubscribeMultipleMailboxRequest) { - consumeState(_subscribeMultipleMailboxInteractor.execute(_accountId, subscribeRequest)); + consumeState(_subscribeMultipleMailboxInteractor.execute(session, accountId, subscribeRequest)); } else if (subscribeRequest is SubscribeMailboxRequest) { - consumeState(_subscribeMailboxInteractor.execute(_accountId, subscribeRequest)); + consumeState(_subscribeMailboxInteractor.execute(session, accountId, subscribeRequest)); } } } void openMailboxAction(BuildContext context, PresentationMailbox mailbox) { - FocusScope.of(context).unfocus(); - dashboardController.openMailboxAction(context, mailbox); + KeyboardUtils.hideKeyboard(context); + dashboardController.openMailboxAction(mailbox); if (!responsiveUtils.isWebDesktop(context)) { closeSearchView(context); @@ -503,7 +540,7 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa {List? listDescendantMailboxIds} ) { if (currentOverlayContext != null && currentContext != null) { - _appToast.showBottomToast( + _appToast.showToastMessage( currentOverlayContext!, subscribeAction.getToastMessageSuccess(currentContext!), actionName: AppLocalizations.of(currentContext!).undo, @@ -520,18 +557,11 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa ); } }, - leadingIcon: SvgPicture.asset( - imagePaths.icFolderMailbox, - width: 24, - height: 24, - color: Colors.white, - fit: BoxFit.fill - ), + leadingSVGIconColor: Colors.white, + leadingSVGIcon: imagePaths.icFolderMailbox, backgroundColor: AppColor.toastSuccessBackgroundColor, textColor: Colors.white, - textActionColor: Colors.white, actionIcon: SvgPicture.asset(imagePaths.icUndo), - maxWidth: responsiveUtils.getMaxWidthToast(currentContext!) ); } } @@ -540,8 +570,9 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa MailboxId mailboxIdSubscribed, {List? listDescendantMailboxIds} ) { - final _accountId = dashboardController.accountId.value; - if (_accountId != null) { + final accountId = dashboardController.accountId.value; + final session = dashboardController.sessionCurrent; + if (session != null && accountId != null) { SubscribeRequest? subscribeRequest; if (listDescendantMailboxIds != null) { @@ -560,9 +591,9 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa } if (subscribeRequest is SubscribeMultipleMailboxRequest) { - consumeState(_subscribeMultipleMailboxInteractor.execute(_accountId, subscribeRequest)); + consumeState(_subscribeMultipleMailboxInteractor.execute(session, accountId, subscribeRequest)); } else if (subscribeRequest is SubscribeMailboxRequest) { - consumeState(_subscribeMailboxInteractor.execute(_accountId, subscribeRequest)); + consumeState(_subscribeMailboxInteractor.execute(session, accountId, subscribeRequest)); } } } @@ -571,8 +602,9 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa MailboxId mailboxIdSubscribed, {List? listDescendantMailboxIds} ) { - final _accountId = dashboardController.accountId.value; - if (_accountId != null) { + final accountId = dashboardController.accountId.value; + final session = dashboardController.sessionCurrent; + if (session != null && accountId != null) { SubscribeRequest? subscribeRequest; if (listDescendantMailboxIds != null) { @@ -591,9 +623,9 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa } if (subscribeRequest is SubscribeMultipleMailboxRequest) { - consumeState(_subscribeMultipleMailboxInteractor.execute(_accountId, subscribeRequest)); + consumeState(_subscribeMultipleMailboxInteractor.execute(session, accountId, subscribeRequest)); } else if (subscribeRequest is SubscribeMailboxRequest) { - consumeState(_subscribeMailboxInteractor.execute(_accountId, subscribeRequest)); + consumeState(_subscribeMailboxInteractor.execute(session, accountId, subscribeRequest)); } } } @@ -610,6 +642,60 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa } } + void goToCreateNewMailboxView(BuildContext context, {PresentationMailbox? parentMailbox}) async { + final accountId = dashboardController.accountId.value; + final session = dashboardController.sessionCurrent; + if (session != null && accountId != null) { + final arguments = MailboxCreatorArguments( + accountId, + defaultMailboxTree.value, + personalMailboxTree.value, + teamMailboxesTree.value, + dashboardController.sessionCurrent!, + parentMailbox + ); + + final result = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.mailboxCreator, arguments: arguments) + : await push(AppRoutes.mailboxCreator, arguments: arguments); + + if (result != null && result is NewMailboxArguments) { + final generateCreateId = Id(_uuid.v1()); + _createNewMailboxAction(session, accountId, CreateNewMailboxRequest( + generateCreateId, + result.newName, + parentId: result.mailboxLocation?.id)); + } + } + } + + void _createNewMailboxAction(Session session, AccountId accountId, CreateNewMailboxRequest request) async { + consumeState(_createNewMailboxInteractor.execute(session, accountId, request)); + } + + void _createNewMailboxSuccess(CreateNewMailboxSuccess success) { + if (currentOverlayContext != null && currentContext != null) { + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).createFolderSuccessfullyMessage(success.newMailbox.name?.name ?? ''), + leadingSVGIconColor: Colors.white, + leadingSVGIcon: imagePaths.icFolderMailbox); + } + + _refreshMailboxChanges(mailboxState: success.currentMailboxState); + } + + void _createNewMailboxFailure(CreateNewMailboxFailure failure) { + if (currentOverlayContext != null && currentContext != null) { + final exception = failure.exception; + var messageError = AppLocalizations.of(currentContext!).create_new_mailbox_failure; + if (exception is ErrorMethodResponse) { + messageError = exception.description ?? AppLocalizations.of(currentContext!).create_new_mailbox_failure; + } + _appToast.showToastErrorMessage(currentOverlayContext!, messageError); + } + } + void clearAllTextInputSearchForm() { textInputSearchController.clear(); currentSearchQuery.value = ''; @@ -617,8 +703,8 @@ class SearchMailboxController extends BaseMailboxController with MailboxActionHa } void closeSearchView(BuildContext context) { - FocusScope.of(context).unfocus(); - if (BuildUtils.isWeb) { + KeyboardUtils.hideKeyboard(context); + if (PlatformInfo.isWeb) { dashboardController.searchMailboxActivated.value = false; clearAllTextInputSearchForm(); SearchMailboxBindings().disposeBindings(); diff --git a/lib/features/search/mailbox/presentation/search_mailbox_view.dart b/lib/features/search/mailbox/presentation/search_mailbox_view.dart index 5697bc032a..e0aa9c88d9 100644 --- a/lib/features/search/mailbox/presentation/search_mailbox_view.dart +++ b/lib/features/search/mailbox/presentation/search_mailbox_view.dart @@ -2,7 +2,8 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/views/button/icon_button_web.dart'; import 'package:core/presentation/views/text/text_field_builder.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:focused_menu_custom/modals.dart'; @@ -35,7 +36,7 @@ class SearchMailboxView extends GetWidget return Scaffold( body: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), - child: BuildUtils.isWeb + child: PlatformInfo.isWeb ? PointerInterceptor(child: _buildSearchBody(context)) : SafeArea(child: _buildSearchBody(context)), ), @@ -82,7 +83,7 @@ class SearchMailboxView extends GetWidget splashRadius: SearchMailboxUtils.getIconSplashRadius(context, controller.responsiveUtils), icon: SvgPicture.asset( controller.imagePaths.icBack, - color: AppColor.colorTextButton, + colorFilter: AppColor.colorTextButton.asFilter(), fit: BoxFit.fill ), tooltip: AppLocalizations.of(context).back, @@ -104,7 +105,7 @@ class SearchMailboxView extends GetWidget iconPadding: EdgeInsets.zero, icon: SvgPicture.asset( controller.imagePaths.icSearchBar, - color: AppColor.colorTextButton, + colorFilter: AppColor.colorTextButton.asFilter(), fit: BoxFit.fill ), tooltip: AppLocalizations.of(context).search, @@ -138,33 +139,29 @@ class SearchMailboxView extends GetWidget } Widget _buildTextFieldSearchInput(BuildContext context) { - return (TextFieldBuilder() - ..onChange(controller.onTextSearchChange) - ..textInputAction(TextInputAction.search) - ..autoFocus(true) - ..addController(controller.textInputSearchController) - ..textStyle(const TextStyle( - color: Colors.black, + return TextFieldBuilder( + onTextChange: controller.onTextSearchChange, + textInputAction: TextInputAction.search, + autoFocus: true, + maxLines: 1, + controller: controller.textInputSearchController, + textDirection: DirectionUtils.getDirectionByLanguage(context), + textStyle: const TextStyle( + color: Colors.black, + fontSize: 15, + fontWeight: FontWeight.normal), + keyboardType: TextInputType.text, + onTextSubmitted: (text) => controller.onTextSearchSubmitted(context, text), + decoration: InputDecoration( + contentPadding: EdgeInsets.zero, + hintText: AppLocalizations.of(context).searchForMailboxes, + hintStyle: const TextStyle( + color: AppColor.loginTextFieldHintColor, fontSize: 15, - fontWeight: FontWeight.normal)) - ..keyboardType(TextInputType.text) - ..onSubmitted((value) { - final query = value.trim(); - if (query.isNotEmpty) { - controller.submitSearchAction(context, query); - } - }) - ..maxLines(1) - ..textDecoration(InputDecoration( - contentPadding: EdgeInsets.zero, - hintText: AppLocalizations.of(context).searchForMailboxes, - hintStyle: const TextStyle( - color: AppColor.loginTextFieldHintColor, - fontSize: 15, - fontWeight: FontWeight.normal), - border: InputBorder.none - ) - )).build(); + fontWeight: FontWeight.normal), + border: InputBorder.none + ), + ); } Widget _buildMailboxListView(BuildContext context) { @@ -195,22 +192,12 @@ class SearchMailboxView extends GetWidget }); } - List _generateListContextMenuItemAction(PresentationMailbox mailbox) { - final mailboxActionsSupported = mailbox.supportedSubscribe - ? _listActionForMailboxSubscribed(mailbox) - : _listActionForMailboxUnsubscribed(mailbox); - + List _listPopupMenuItemAction(BuildContext context, PresentationMailbox mailbox) { final contextMenuActions = listContextMenuItemAction( mailbox, - controller.dashboardController, - mailboxActions: mailboxActionsSupported + controller.dashboardController.enableSpamReport, ); - - return contextMenuActions; - } - - List _listPopupMenuItemAction(BuildContext context, PresentationMailbox mailbox) { - return _generateListContextMenuItemAction(mailbox) + return contextMenuActions .map((action) => _mailboxFocusedMenuItem(context, action, mailbox)) .toList(); } @@ -239,7 +226,7 @@ class SearchMailboxView extends GetWidget width: 24, height: 24, fit: BoxFit.fill, - color: contextMenuItem.action.getColorContextMenuIcon() + colorFilter: contextMenuItem.action.getColorContextMenuIcon().asFilter() ), const SizedBox(width: 12), Expanded(child: Text( @@ -257,23 +244,19 @@ class SearchMailboxView extends GetWidget ); } - List _listActionForMailboxUnsubscribed(PresentationMailbox mailbox) { - return [ - if (mailbox.isSupportedEnableMailbox) - MailboxActions.enableMailbox - ]; - } - - List _listActionForMailboxSubscribed(PresentationMailbox mailbox) { - return listActionForMailbox(mailbox, controller.dashboardController); - } - void _openMailboxMenuAction( BuildContext context, PresentationMailbox mailbox, {RelativeRect? position} ) { - final contextMenuActions = _generateListContextMenuItemAction(mailbox); + final contextMenuActions = listContextMenuItemAction( + mailbox, + controller.dashboardController.enableSpamReport, + ); + + if (contextMenuActions.isEmpty) { + return; + } if (controller.responsiveUtils.isScreenWithShortestSide(context) || position == null) { controller.openContextMenuAction( diff --git a/lib/features/search/mailbox/presentation/widgets/mailbox_searched_item_builder.dart b/lib/features/search/mailbox/presentation/widgets/mailbox_searched_item_builder.dart index 8dbd7cfe86..4b36dbcf6f 100644 --- a/lib/features/search/mailbox/presentation/widgets/mailbox_searched_item_builder.dart +++ b/lib/features/search/mailbox/presentation/widgets/mailbox_searched_item_builder.dart @@ -1,16 +1,17 @@ import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; -import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/responsive/responsive_widget.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/presentation/views/text/text_overflow_builder.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:focused_menu_custom/focused_menu.dart'; import 'package:focused_menu_custom/modals.dart'; import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; -import 'package:tmail_ui_user/features/mailbox/domain/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/mailbox/presentation/utils/mailbox_method_action_define.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/utils/search_mailbox_utils.dart'; @@ -50,11 +51,9 @@ class _MailboxSearchedItemBuilderState extends State @override Widget build(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return DragTarget>( - builder: (_, __, ___) { - return _buildMailboxItem(context); - }, + builder: (_, __, ___) => _buildMailboxItem(context), onAccept: (emails) { widget.onDragEmailToMailboxAccepted?.call(emails, widget._presentationMailbox); } @@ -65,7 +64,7 @@ class _MailboxSearchedItemBuilderState extends State } Widget _buildMailboxItem(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return InkWell( onTap: _onTapMailboxAction, onHover: (value) => setState(() => isHoverItem = value), @@ -87,7 +86,7 @@ class _MailboxSearchedItemBuilderState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildTitleItem(), + _buildTitleItem(context), _buildSubtitleItem() ] ) @@ -135,7 +134,7 @@ class _MailboxSearchedItemBuilderState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildTitleItem(), + _buildTitleItem(context), _buildSubtitleItem() ] ) @@ -169,7 +168,7 @@ class _MailboxSearchedItemBuilderState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildTitleItem(), + _buildTitleItem(context), _buildSubtitleItem() ] ) @@ -182,18 +181,20 @@ class _MailboxSearchedItemBuilderState extends State } void _onTapMailboxAction() { - if (widget._presentationMailbox.isSubscribed?.value == true) { + if (widget._presentationMailbox.allowedToDisplay) { widget.onClickOpenMailboxAction?.call(widget._presentationMailbox); } } void _onLongPressMailboxAction() { - widget.onLongPressMailboxAction?.call(widget._presentationMailbox); + if (widget.listPopupMenuItemAction?.isNotEmpty == true) { + widget.onLongPressMailboxAction?.call(widget._presentationMailbox); + } } Widget _buildMailboxIcon() { return SvgPicture.asset( - widget._presentationMailbox.isSubscribed?.value == true + widget._presentationMailbox.allowedToDisplay ? widget._presentationMailbox.getMailboxIcon(widget._imagePaths) : widget._imagePaths.icHideFolder, width: 20, @@ -202,12 +203,9 @@ class _MailboxSearchedItemBuilderState extends State ); } - Widget _buildTitleItem() { - return Text( - widget._presentationMailbox.name?.name ?? '', - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, + Widget _buildTitleItem(BuildContext context) { + return TextOverflowBuilder( + widget._presentationMailbox.getDisplayName(context), style: const TextStyle( fontSize: 15, color: Colors.black @@ -217,11 +215,8 @@ class _MailboxSearchedItemBuilderState extends State Widget _buildSubtitleItem() { if (widget._presentationMailbox.mailboxPath?.isNotEmpty == true) { - return Text( - widget._presentationMailbox.mailboxPath ?? '', - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, + return TextOverflowBuilder( + (widget._presentationMailbox.mailboxPath ?? ''), style: const TextStyle( fontSize: 11, color: AppColor.colorMailboxPath, @@ -229,11 +224,8 @@ class _MailboxSearchedItemBuilderState extends State ), ); } else if (widget._presentationMailbox.isTeamMailboxes) { - return Text( - widget._presentationMailbox.emailTeamMailBoxes ?? '', - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, + return TextOverflowBuilder( + widget._presentationMailbox.emailTeamMailBoxes, style: const TextStyle( fontSize: 11, color: AppColor.colorEmailAddressFull, diff --git a/lib/features/sending_queue/data/exceptions/sending_queue_exceptions.dart b/lib/features/sending_queue/data/exceptions/sending_queue_exceptions.dart new file mode 100644 index 0000000000..ab1649479c --- /dev/null +++ b/lib/features/sending_queue/data/exceptions/sending_queue_exceptions.dart @@ -0,0 +1,4 @@ + +class NotFoundSendingEmailHiveObject implements Exception {} + +class ExistSendingEmailHiveObject implements Exception {} \ No newline at end of file diff --git a/lib/features/sending_queue/data/repository/sending_queue_repository_impl.dart b/lib/features/sending_queue/data/repository/sending_queue_repository_impl.dart new file mode 100644 index 0000000000..f2e757a427 --- /dev/null +++ b/lib/features/sending_queue/data/repository/sending_queue_repository_impl.dart @@ -0,0 +1,47 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/repository/sending_queue_repository.dart'; + +class SendingQueueRepositoryImpl extends SendingQueueRepository { + + final EmailHiveCacheDataSourceImpl _emailHiveCacheDataSourceImpl; + + SendingQueueRepositoryImpl(this._emailHiveCacheDataSourceImpl); + + @override + Future> getAllSendingEmails(AccountId accountId, UserName userName) { + return _emailHiveCacheDataSourceImpl.getAllSendingEmails(accountId, userName); + } + + @override + Future deleteSendingEmail(AccountId accountId, UserName userName, String sendingId) { + return _emailHiveCacheDataSourceImpl.deleteSendingEmail(accountId, userName, sendingId); + } + + @override + Future updateSendingEmail(AccountId accountId, UserName userName, SendingEmail newSendingEmail) { + return _emailHiveCacheDataSourceImpl.updateSendingEmail(accountId, userName, newSendingEmail); + } + + @override + Future storeSendingEmail(AccountId accountId, UserName userName, SendingEmail sendingEmail) { + return _emailHiveCacheDataSourceImpl.storeSendingEmail(accountId, userName, sendingEmail); + } + + @override + Future> updateMultipleSendingEmail(AccountId accountId, UserName userName, List newSendingEmails) { + return _emailHiveCacheDataSourceImpl.updateMultipleSendingEmail(accountId, userName, newSendingEmails); + } + + @override + Future deleteMultipleSendingEmail(AccountId accountId, UserName userName, List sendingIds) { + return _emailHiveCacheDataSourceImpl.deleteMultipleSendingEmail(accountId, userName, sendingIds); + } + + @override + Future getStoredSendingEmail(AccountId accountId, UserName userName, String sendingId) { + return _emailHiveCacheDataSourceImpl.getStoredSendingEmail(accountId, userName, sendingId); + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/extensions/list_sending_email_extension.dart b/lib/features/sending_queue/domain/extensions/list_sending_email_extension.dart new file mode 100644 index 0000000000..950cf71c92 --- /dev/null +++ b/lib/features/sending_queue/domain/extensions/list_sending_email_extension.dart @@ -0,0 +1,9 @@ +import 'package:tmail_ui_user/features/offline_mode/model/sending_email_hive_cache.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +extension ListSendingEmailExtension on List { + List toHiveCache() => map((sendingEmail) => sendingEmail.toHiveCache()).toList(); + + List get sendingIds => map((sendingEmail) => sendingEmail.sendingId).toSet().toList(); +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/extensions/sending_email_extension.dart b/lib/features/sending_queue/domain/extensions/sending_email_extension.dart new file mode 100644 index 0000000000..cd92086701 --- /dev/null +++ b/lib/features/sending_queue/domain/extensions/sending_email_extension.dart @@ -0,0 +1,91 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/extensions/email_extension.dart'; +import 'package:model/extensions/email_id_extensions.dart'; +import 'package:model/extensions/identity_id_extension.dart'; +import 'package:model/extensions/mailbox_id_extension.dart'; +import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_email_hive_cache.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +extension SendingEmailExtension on SendingEmail { + SendingEmailHiveCache toHiveCache() { + return SendingEmailHiveCache( + sendingId, + email.asString(), + emailActionType.name, + createTime, + sentMailboxId?.asString, + emailIdDestroyed?.asString, + emailIdAnsweredOrForwarded?.asString, + identityId?.asString, + mailboxNameRequest?.name, + creationIdRequest?.value, + sendingState.name, + ); + } + + EmailRequest toEmailRequest({Email? newEmail}) { + return EmailRequest( + email: newEmail ?? email, + emailActionType: emailActionType, + sentMailboxId: sentMailboxId, + emailIdDestroyed: emailIdDestroyed, + emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, + identityId: identityId, + storedSendingId: sendingId + ); + } + + SendingEmail toggleSelection() { + return SendingEmail( + sendingId: sendingId, + email: email, + emailActionType: emailActionType, + createTime: createTime, + sentMailboxId: sentMailboxId, + emailIdDestroyed: emailIdDestroyed, + emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, + identityId: identityId, + mailboxNameRequest: mailboxNameRequest, + creationIdRequest: creationIdRequest, + sendingState: sendingState, + selectMode: selectMode == SelectMode.INACTIVE ? SelectMode.ACTIVE : SelectMode.INACTIVE + ); + } + + SendingEmail unSelected() { + return SendingEmail( + sendingId: sendingId, + email: email, + emailActionType: emailActionType, + createTime: createTime, + sentMailboxId: sentMailboxId, + emailIdDestroyed: emailIdDestroyed, + emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, + identityId: identityId, + mailboxNameRequest: mailboxNameRequest, + creationIdRequest: creationIdRequest, + sendingState: sendingState, + selectMode: SelectMode.INACTIVE + ); + } + + SendingEmail updatingSendingState(SendingState newState) { + return SendingEmail( + sendingId: sendingId, + email: email, + emailActionType: emailActionType, + createTime: createTime, + sentMailboxId: sentMailboxId, + emailIdDestroyed: emailIdDestroyed, + emailIdAnsweredOrForwarded: emailIdAnsweredOrForwarded, + identityId: identityId, + mailboxNameRequest: mailboxNameRequest, + creationIdRequest: creationIdRequest, + sendingState: newState, + selectMode: selectMode + ); + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/model/sending_email.dart b/lib/features/sending_queue/domain/model/sending_email.dart new file mode 100644 index 0000000000..fd54ff2373 --- /dev/null +++ b/lib/features/sending_queue/domain/model/sending_email.dart @@ -0,0 +1,129 @@ +import 'dart:convert'; +import 'package:core/domain/extensions/datetime_extension.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_date_range_picker/flutter_date_range_picker.dart'; +import 'package:jmap_dart_client/http/converter/email_id_nullable_converter.dart'; +import 'package:jmap_dart_client/http/converter/id_nullable_converter.dart'; +import 'package:jmap_dart_client/http/converter/identities/identity_id_nullable_converter.dart'; +import 'package:jmap_dart_client/http/converter/mailbox_id_nullable_converter.dart'; +import 'package:jmap_dart_client/http/converter/mailbox_name_converter.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/email_extension.dart'; +import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_state.dart'; + +class SendingEmail with EquatableMixin { + final String sendingId; + final Email email; + final MailboxId? sentMailboxId; + final EmailId? emailIdDestroyed; + final EmailId? emailIdAnsweredOrForwarded; + final IdentityId? identityId; + final EmailActionType emailActionType; + final MailboxName? mailboxNameRequest; + final Id? creationIdRequest; + final DateTime createTime; + final SelectMode selectMode; + final SendingState sendingState; + + SendingEmail({ + required this.sendingId, + required this.email, + required this.emailActionType, + required this.createTime, + this.sentMailboxId, + this.emailIdDestroyed, + this.emailIdAnsweredOrForwarded, + this.identityId, + this.mailboxNameRequest, + this.creationIdRequest, + this.selectMode = SelectMode.INACTIVE, + this.sendingState = SendingState.waiting + }); + + Map toJson() { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('sendingId', sendingId); + writeNotNull('email', email.asString()); + writeNotNull('emailActionType', emailActionType.name); + writeNotNull('createTime', createTime.toIso8601String()); + writeNotNull('sentMailboxId', const MailboxIdNullableConverter().toJson(sentMailboxId)); + writeNotNull('emailIdDestroyed', const EmailIdNullableConverter().toJson(emailIdDestroyed)); + writeNotNull('emailIdAnsweredOrForwarded', const EmailIdNullableConverter().toJson(emailIdAnsweredOrForwarded)); + writeNotNull('identityId', const IdentityIdNullableConverter().toJson(identityId)); + writeNotNull('mailboxNameRequest', mailboxNameRequest?.name); + writeNotNull('creationIdRequest', const IdNullableConverter().toJson(creationIdRequest)); + + return val; + } + + PresentationEmail get presentationEmail => email.sendingEmailToPresentationEmail(); + + String getCreateTimeAt(String newLocale) { + return DateFormat(createTime.toPattern(), newLocale).format(createTime); + } + + factory SendingEmail.fromJson(Map json) { + return SendingEmail( + sendingId: json['sendingId'] as String, + email: Email.fromJson(jsonDecode(json['email'])), + emailActionType: _getEmailActionType(json['emailActionType'] as String), + createTime: DateTime.parse(json['createTime'] as String), + sentMailboxId: const MailboxIdNullableConverter().fromJson(json['sentMailboxId'] as String?), + emailIdDestroyed: const EmailIdNullableConverter().fromJson(json['emailIdDestroyed'] as String?), + emailIdAnsweredOrForwarded: const EmailIdNullableConverter().fromJson(json['emailIdAnsweredOrForwarded'] as String?), + identityId: const IdentityIdNullableConverter().fromJson(json['identityId'] as String?), + mailboxNameRequest: const MailboxNameConverter().fromJson(json['mailboxNameRequest'] as String?), + creationIdRequest: const IdNullableConverter().fromJson(json['creationIdRequest'] as String?), + ); + } + + static EmailActionType _getEmailActionType(String value) { + return EmailActionType.values.firstWhere( + (type) => type.name == value, + orElse: () => throw ArgumentError('Invalid email action type: $value'), + ); + } + + bool get isSelected => selectMode == SelectMode.ACTIVE; + + bool get isWaiting => sendingState == SendingState.waiting; + + bool get isError => sendingState == SendingState.error; + + bool get isSuccess => sendingState == SendingState.success; + + bool get isCanceled => sendingState == SendingState.canceled; + + bool get isRunning => sendingState == SendingState.running; + + bool get isEditableSupported => isWaiting || isRunning || isCanceled; + + @override + List get props => [ + sendingId, + email, + emailActionType, + createTime, + sentMailboxId, + emailIdDestroyed, + emailIdAnsweredOrForwarded, + identityId, + mailboxNameRequest, + creationIdRequest, + selectMode, + sendingState, + ]; +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/repository/sending_queue_repository.dart b/lib/features/sending_queue/domain/repository/sending_queue_repository.dart new file mode 100644 index 0000000000..bf85b72d8f --- /dev/null +++ b/lib/features/sending_queue/domain/repository/sending_queue_repository.dart @@ -0,0 +1,20 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +abstract class SendingQueueRepository { + Future> getAllSendingEmails(AccountId accountId, UserName userName); + + Future storeSendingEmail(AccountId accountId, UserName userName, SendingEmail sendingEmail); + + Future deleteSendingEmail(AccountId accountId, UserName userName, String sendingId); + + Future deleteMultipleSendingEmail(AccountId accountId, UserName userName, List sendingIds); + + Future updateSendingEmail(AccountId accountId, UserName userName, SendingEmail newSendingEmail); + + Future> updateMultipleSendingEmail(AccountId accountId, UserName userName, List newSendingEmails); + + Future getStoredSendingEmail(AccountId accountId, UserName userName, String sendingId); +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/state/delete_multiple_sending_email_state.dart b/lib/features/sending_queue/domain/state/delete_multiple_sending_email_state.dart new file mode 100644 index 0000000000..a561cf0818 --- /dev/null +++ b/lib/features/sending_queue/domain/state/delete_multiple_sending_email_state.dart @@ -0,0 +1,19 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class DeleteMultipleSendingEmailLoading extends UIState {} + +class DeleteMultipleSendingEmailSuccess extends UIState { + final List sendingIds; + + DeleteMultipleSendingEmailSuccess(this.sendingIds); + + @override + List get props => [sendingIds]; +} + +class DeleteMultipleSendingEmailFailure extends FeatureFailure { + + DeleteMultipleSendingEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/state/delete_sending_email_state.dart b/lib/features/sending_queue/domain/state/delete_sending_email_state.dart new file mode 100644 index 0000000000..e1018fb7fc --- /dev/null +++ b/lib/features/sending_queue/domain/state/delete_sending_email_state.dart @@ -0,0 +1,11 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class DeleteSendingEmailLoading extends UIState {} + +class DeleteSendingEmailSuccess extends UIState {} + +class DeleteSendingEmailFailure extends FeatureFailure { + + DeleteSendingEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/state/get_all_sending_email_state.dart b/lib/features/sending_queue/domain/state/get_all_sending_email_state.dart new file mode 100644 index 0000000000..6acbbdc83e --- /dev/null +++ b/lib/features/sending_queue/domain/state/get_all_sending_email_state.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +class GetAllSendingEmailLoading extends UIState {} + +class GetAllSendingEmailSuccess extends UIState { + + final List sendingEmails; + + GetAllSendingEmailSuccess(this.sendingEmails); + + @override + List get props => [sendingEmails]; +} + +class GetAllSendingEmailFailure extends FeatureFailure { + + GetAllSendingEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/state/get_stored_sending_email_state.dart b/lib/features/sending_queue/domain/state/get_stored_sending_email_state.dart new file mode 100644 index 0000000000..e327ef1bc6 --- /dev/null +++ b/lib/features/sending_queue/domain/state/get_stored_sending_email_state.dart @@ -0,0 +1,37 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +class GetStoredSendingEmailLoading extends UIState {} + +class GetStoredSendingEmailSuccess extends UIState { + + final SendingEmail sendingEmail; + final AccountId accountId; + final UserName userName; + final SendingState sendingState; + + GetStoredSendingEmailSuccess( + this.sendingEmail, + this.accountId, + this.userName, + this.sendingState + ); + + @override + List get props => [ + sendingEmail, + accountId, + userName, + sendingState + ]; +} + +class GetStoredSendingEmailFailure extends FeatureFailure { + + GetStoredSendingEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/state/store_sending_email_state.dart b/lib/features/sending_queue/domain/state/store_sending_email_state.dart new file mode 100644 index 0000000000..4d8b82287a --- /dev/null +++ b/lib/features/sending_queue/domain/state/store_sending_email_state.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +class StoreSendingEmailLoading extends UIState {} + +class StoreSendingEmailSuccess extends UIState { + + final SendingEmail sendingEmail; + + StoreSendingEmailSuccess(this.sendingEmail); + + @override + List get props => [sendingEmail]; +} + +class StoreSendingEmailFailure extends FeatureFailure { + + StoreSendingEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/state/update_multiple_sending_email_state.dart b/lib/features/sending_queue/domain/state/update_multiple_sending_email_state.dart new file mode 100644 index 0000000000..af8a57018d --- /dev/null +++ b/lib/features/sending_queue/domain/state/update_multiple_sending_email_state.dart @@ -0,0 +1,34 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +class UpdateMultipleSendingEmailLoading extends UIState {} + +class UpdateMultipleSendingEmailAllSuccess extends UIState { + final List newSendingEmails; + + UpdateMultipleSendingEmailAllSuccess(this.newSendingEmails); + + @override + List get props => [newSendingEmails]; +} + +class UpdateMultipleSendingEmailHasSomeSuccess extends UIState { + final List newSendingEmails; + + UpdateMultipleSendingEmailHasSomeSuccess(this.newSendingEmails); + + @override + List get props => [newSendingEmails]; +} + +class UpdateMultipleSendingEmailAllFailure extends FeatureFailure { + + UpdateMultipleSendingEmailAllFailure(dynamic exception) : super(exception: exception); +} + +class UpdateMultipleSendingEmailFailure extends FeatureFailure { + + UpdateMultipleSendingEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/state/update_sending_email_state.dart b/lib/features/sending_queue/domain/state/update_sending_email_state.dart new file mode 100644 index 0000000000..96eb5aaf64 --- /dev/null +++ b/lib/features/sending_queue/domain/state/update_sending_email_state.dart @@ -0,0 +1,20 @@ + +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +class UpdateSendingEmailLoading extends UIState {} + +class UpdateSendingEmailSuccess extends UIState { + final SendingEmail newSendingEmail; + + UpdateSendingEmailSuccess(this.newSendingEmail); + + @override + List get props => [newSendingEmail]; +} + +class UpdateSendingEmailFailure extends FeatureFailure { + + UpdateSendingEmailFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/usecases/delete_multiple_sending_email_interactor.dart b/lib/features/sending_queue/domain/usecases/delete_multiple_sending_email_interactor.dart new file mode 100644 index 0000000000..a2b1ed87b8 --- /dev/null +++ b/lib/features/sending_queue/domain/usecases/delete_multiple_sending_email_interactor.dart @@ -0,0 +1,23 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/repository/sending_queue_repository.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/state/delete_multiple_sending_email_state.dart'; + +class DeleteMultipleSendingEmailInteractor { + final SendingQueueRepository _sendingQueueRepository; + + DeleteMultipleSendingEmailInteractor(this._sendingQueueRepository); + + Stream> execute(AccountId accountId, UserName userName, List sendingIds) async* { + try { + yield Right(DeleteMultipleSendingEmailLoading()); + await _sendingQueueRepository.deleteMultipleSendingEmail(accountId, userName, sendingIds); + yield Right(DeleteMultipleSendingEmailSuccess(sendingIds)); + } catch (e) { + yield Left(DeleteMultipleSendingEmailFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/usecases/delete_sending_email_interactor.dart b/lib/features/sending_queue/domain/usecases/delete_sending_email_interactor.dart new file mode 100644 index 0000000000..2679314685 --- /dev/null +++ b/lib/features/sending_queue/domain/usecases/delete_sending_email_interactor.dart @@ -0,0 +1,23 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_sending_email_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/repository/sending_queue_repository.dart'; + +class DeleteSendingEmailInteractor { + final SendingQueueRepository _sendingQueueRepository; + + DeleteSendingEmailInteractor(this._sendingQueueRepository); + + Stream> execute(AccountId accountId, UserName userName, String sendingId) async* { + try { + yield Right(DeleteSendingEmailLoading()); + await _sendingQueueRepository.deleteSendingEmail(accountId, userName, sendingId); + yield Right(DeleteSendingEmailSuccess()); + } catch (e) { + yield Left(DeleteSendingEmailFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/usecases/get_all_sending_email_interactor.dart b/lib/features/sending_queue/domain/usecases/get_all_sending_email_interactor.dart new file mode 100644 index 0000000000..44daca729e --- /dev/null +++ b/lib/features/sending_queue/domain/usecases/get_all_sending_email_interactor.dart @@ -0,0 +1,25 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/repository/sending_queue_repository.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/state/get_all_sending_email_state.dart'; + +class GetAllSendingEmailInteractor { + final SendingQueueRepository _sendingQueueRepository; + + GetAllSendingEmailInteractor(this._sendingQueueRepository); + + Stream> execute(AccountId accountId, UserName userName) async* { + try { + yield Right(GetAllSendingEmailLoading()); + final sendingEmails = await _sendingQueueRepository.getAllSendingEmails(accountId, userName); + log('GetAllSendingEmailInteractor::execute():sendingEmails: ${sendingEmails.map((e) => '${e.email.subject} | ${e.sendingState}')}'); + yield Right(GetAllSendingEmailSuccess(sendingEmails)); + } catch (e) { + yield Left(GetAllSendingEmailFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/usecases/get_stored_sending_email_interactor.dart b/lib/features/sending_queue/domain/usecases/get_stored_sending_email_interactor.dart new file mode 100644 index 0000000000..3ada1ca850 --- /dev/null +++ b/lib/features/sending_queue/domain/usecases/get_stored_sending_email_interactor.dart @@ -0,0 +1,36 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/repository/sending_queue_repository.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/state/get_stored_sending_email_state.dart'; + +class GetStoredSendingEmailInteractor { + final SendingQueueRepository _sendingQueueRepository; + + GetStoredSendingEmailInteractor(this._sendingQueueRepository); + + Stream> execute( + AccountId accountId, + UserName userName, + String sendingId, + SendingState sendingState + ) async* { + try { + yield Right(GetStoredSendingEmailLoading()); + final sendingEmail = await _sendingQueueRepository.getStoredSendingEmail(accountId, userName, sendingId); + yield Right( + GetStoredSendingEmailSuccess( + sendingEmail, + accountId, + userName, + sendingState + ) + ); + } catch (e) { + yield Left(GetStoredSendingEmailFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/usecases/store_sending_email_interactor.dart b/lib/features/sending_queue/domain/usecases/store_sending_email_interactor.dart new file mode 100644 index 0000000000..3b23cd27c1 --- /dev/null +++ b/lib/features/sending_queue/domain/usecases/store_sending_email_interactor.dart @@ -0,0 +1,28 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/email/domain/state/store_sending_email_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/repository/sending_queue_repository.dart'; + +class StoreSendingEmailInteractor { + final SendingQueueRepository _sendingQueueRepository; + + StoreSendingEmailInteractor(this._sendingQueueRepository); + + Stream> execute( + AccountId accountId, + UserName userName, + SendingEmail sendingEmail, + ) async* { + try { + yield Right(StoreSendingEmailLoading()); + final storedSendingEmail = await _sendingQueueRepository.storeSendingEmail(accountId, userName, sendingEmail); + yield Right(StoreSendingEmailSuccess(storedSendingEmail)); + } catch (e) { + yield Left(StoreSendingEmailFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/usecases/update_multiple_sending_email_interactor.dart b/lib/features/sending_queue/domain/usecases/update_multiple_sending_email_interactor.dart new file mode 100644 index 0000000000..95b1143097 --- /dev/null +++ b/lib/features/sending_queue/domain/usecases/update_multiple_sending_email_interactor.dart @@ -0,0 +1,38 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/sending_queue/data/exceptions/sending_queue_exceptions.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/repository/sending_queue_repository.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/state/update_multiple_sending_email_state.dart'; + +class UpdateMultipleSendingEmailInteractor { + final SendingQueueRepository _sendingQueueRepository; + + UpdateMultipleSendingEmailInteractor(this._sendingQueueRepository); + + Stream> execute( + AccountId accountId, + UserName userName, + List newSendingEmails + ) async* { + try { + yield Right(UpdateMultipleSendingEmailLoading()); + final storedSendingEmails = await _sendingQueueRepository.updateMultipleSendingEmail( + accountId, + userName, + newSendingEmails); + if (storedSendingEmails.length == newSendingEmails.length) { + yield Right(UpdateMultipleSendingEmailAllSuccess(storedSendingEmails)); + } else if (storedSendingEmails.isEmpty) { + yield Left(UpdateMultipleSendingEmailAllFailure(NotFoundSendingEmailHiveObject())); + } else { + yield Right(UpdateMultipleSendingEmailHasSomeSuccess(storedSendingEmails)); + } + } catch (e) { + yield Left(UpdateMultipleSendingEmailFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/domain/usecases/update_sending_email_interactor.dart b/lib/features/sending_queue/domain/usecases/update_sending_email_interactor.dart new file mode 100644 index 0000000000..f8b1dd5dbb --- /dev/null +++ b/lib/features/sending_queue/domain/usecases/update_sending_email_interactor.dart @@ -0,0 +1,28 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/repository/sending_queue_repository.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/state/update_sending_email_state.dart'; + +class UpdateSendingEmailInteractor { + final SendingQueueRepository _sendingQueueRepository; + + UpdateSendingEmailInteractor(this._sendingQueueRepository); + + Stream> execute( + AccountId accountId, + UserName userName, + SendingEmail newSendingEmail + ) async* { + try { + yield Right(UpdateSendingEmailLoading()); + final storedSendingEmail = await _sendingQueueRepository.updateSendingEmail(accountId, userName, newSendingEmail); + yield Right(UpdateSendingEmailSuccess(storedSendingEmail)); + } catch (e) { + yield Left(UpdateSendingEmailFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/bindings/sending_queue_bindings.dart b/lib/features/sending_queue/presentation/bindings/sending_queue_bindings.dart new file mode 100644 index 0000000000..b5fc2e9573 --- /dev/null +++ b/lib/features/sending_queue/presentation/bindings/sending_queue_bindings.dart @@ -0,0 +1,26 @@ + +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/delete_multiple_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/delete_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/get_stored_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/update_multiple_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/update_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/sending_queue_controller.dart'; + +class SendingQueueBindings extends Bindings { + + @override + void dependencies() { + _bindingsController(); + } + + void _bindingsController() { + Get.put(SendingQueueController( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + )); + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/bindings/sending_queue_interactor_bindings.dart b/lib/features/sending_queue/presentation/bindings/sending_queue_interactor_bindings.dart new file mode 100644 index 0000000000..3a24df36b5 --- /dev/null +++ b/lib/features/sending_queue/presentation/bindings/sending_queue_interactor_bindings.dart @@ -0,0 +1,61 @@ + +import 'package:core/utils/file_utils.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; +import 'package:tmail_ui_user/features/email/data/datasource_impl/email_hive_cache_datasource_impl.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/sending_queue/data/repository/sending_queue_repository_impl.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/repository/sending_queue_repository.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/delete_multiple_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/delete_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/get_all_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/get_stored_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/store_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/update_multiple_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/update_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart'; +import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; + +class SendingQueueInteractorBindings extends InteractorsBindings { + + @override + void bindingsDataSource() {} + + @override + void bindingsDataSourceImpl() { + Get.lazyPut(() => EmailHiveCacheDataSourceImpl( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find())); + } + + @override + void bindingsInteractor() { + Get.lazyPut(() => StoreSendingEmailInteractor(Get.find())); + Get.lazyPut(() => GetAllSendingEmailInteractor(Get.find())); + Get.lazyPut(() => DeleteMultipleSendingEmailInteractor(Get.find())); + Get.lazyPut(() => DeleteSendingEmailInteractor(Get.find())); + Get.lazyPut(() => UpdateSendingEmailInteractor(Get.find())); + Get.lazyPut(() => UpdateMultipleSendingEmailInteractor(Get.find())); + Get.lazyPut(() => GetStoredSendingEmailInteractor(Get.find())); + } + + @override + void bindingsRepository() { + Get.lazyPut(() => Get.find()); + } + + @override + void bindingsRepositoryImpl() { + Get.lazyPut(() => SendingQueueRepositoryImpl(Get.find())); + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/extensions/list_sending_email_extension.dart b/lib/features/sending_queue/presentation/extensions/list_sending_email_extension.dart new file mode 100644 index 0000000000..e3d77db636 --- /dev/null +++ b/lib/features/sending_queue/presentation/extensions/list_sending_email_extension.dart @@ -0,0 +1,24 @@ + +import 'package:tmail_ui_user/features/offline_mode/model/sending_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; + +extension ListSendingEmailExtension on List { + List toggleSelection({required SendingEmail sendingEmailSelected}) => + map((sendingEmail) => sendingEmailSelected.sendingId == sendingEmail.sendingId + ? sendingEmail.toggleSelection() + : sendingEmail + ).toList(); + + List unAllSelected() => map((sendingEmail) => sendingEmail.unSelected()).toList(); + + bool isAllSelected() => every((sendingEmail) => sendingEmail.isSelected); + + bool isAllUnSelected() => every((sendingEmail) => !sendingEmail.isSelected); + + List listSelected() => where((sendingEmail) => sendingEmail.isSelected).toList(); + + bool isAllSendingStateError() => every((sendingEmail) => sendingEmail.isError); + + List toSendingStateWaiting() => map((sendingEmail) => sendingEmail.updatingSendingState(SendingState.waiting)).toList(); +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/model/sending_email_action_type.dart b/lib/features/sending_queue/presentation/model/sending_email_action_type.dart new file mode 100644 index 0000000000..d3bcf417ec --- /dev/null +++ b/lib/features/sending_queue/presentation/model/sending_email_action_type.dart @@ -0,0 +1,62 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/state/button_state.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +enum SendingEmailActionType { + create, + edit, + resend, + delete; + + String getButtonTitle(BuildContext context) { + switch(this) { + case SendingEmailActionType.delete: + return AppLocalizations.of(context).delete; + case SendingEmailActionType.edit: + return AppLocalizations.of(context).edit; + case SendingEmailActionType.create: + return ''; + case SendingEmailActionType.resend: + return AppLocalizations.of(context).resend; + } + } + + String getButtonKey() { + switch(this) { + case SendingEmailActionType.delete: + return 'button_delete_sending_email'; + case SendingEmailActionType.edit: + return 'button_edit_sending_email'; + case SendingEmailActionType.create: + return ''; + case SendingEmailActionType.resend: + return 'button_resend_sending_email'; + } + } + + Color getButtonIconColor(ButtonState buttonState) { + switch(this) { + case SendingEmailActionType.delete: + return AppColor.colorDeletePermanentlyButton.withOpacity(buttonState.opacity); + case SendingEmailActionType.edit: + case SendingEmailActionType.resend: + return AppColor.primaryColor.withOpacity(buttonState.opacity); + case SendingEmailActionType.create: + return Colors.transparent.withOpacity(buttonState.opacity); + } + } + + Color getButtonTitleColor(ButtonState buttonState) { + switch(this) { + case SendingEmailActionType.delete: + return AppColor.colorDeletePermanentlyButton.withOpacity(buttonState.opacity); + case SendingEmailActionType.edit: + case SendingEmailActionType.resend: + return AppColor.primaryColor.withOpacity(buttonState.opacity); + case SendingEmailActionType.create: + return Colors.transparent.withOpacity(buttonState.opacity); + } + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/model/sending_email_arguments.dart b/lib/features/sending_queue/presentation/model/sending_email_arguments.dart new file mode 100644 index 0000000000..af326cb004 --- /dev/null +++ b/lib/features/sending_queue/presentation/model/sending_email_arguments.dart @@ -0,0 +1,33 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/composer/domain/model/email_request.dart'; +import 'package:tmail_ui_user/features/composer/presentation/model/compose_action_mode.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/model/create_new_mailbox_request.dart'; +import 'package:tmail_ui_user/main/routes/router_arguments.dart'; + +class SendingEmailArguments extends RouterArguments { + final Session session; + final AccountId accountId; + final EmailRequest emailRequest; + final CreateNewMailboxRequest? mailboxRequest; + final ComposeActionMode actionMode; + + SendingEmailArguments( + this.session, + this.accountId, + this.emailRequest, + this.mailboxRequest, + { + this.actionMode = ComposeActionMode.sent + } + ); + + @override + List get props => [ + session, + accountId, + emailRequest, + mailboxRequest, + actionMode + ]; +} diff --git a/lib/features/sending_queue/presentation/sending_queue_controller.dart b/lib/features/sending_queue/presentation/sending_queue_controller.dart new file mode 100644 index 0000000000..95e70b129b --- /dev/null +++ b/lib/features/sending_queue/presentation/sending_queue_controller.dart @@ -0,0 +1,372 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/base/base_controller.dart'; +import 'package:tmail_ui_user/features/base/mixin/message_dialog_action_mixin.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; +import 'package:tmail_ui_user/features/email/domain/state/delete_sending_email_state.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/mailbox_dashboard_controller.dart'; +import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' + if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; +import 'package:tmail_ui_user/features/offline_mode/controller/work_manager_controller.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/extensions/list_sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/extensions/sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/state/get_stored_sending_email_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/state/update_multiple_sending_email_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/state/update_sending_email_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/delete_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/get_stored_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/update_multiple_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/update_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/state/delete_multiple_sending_email_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/usecases/delete_multiple_sending_email_interactor.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/extensions/list_sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class SendingQueueController extends BaseController with MessageDialogActionMixin { + + final DeleteMultipleSendingEmailInteractor _deleteMultipleSendingEmailInteractor; + final UpdateMultipleSendingEmailInteractor _updateMultipleSendingEmailInteractor; + final UpdateSendingEmailInteractor _updateSendingEmailInteractor; + final DeleteSendingEmailInteractor _deleteSendingEmailInteractor; + final GetStoredSendingEmailInteractor _getStoredSendingEmailInteractor; + + final dashboardController = Get.find(); + final _networkConnectionController = Get.find(); + final _sendingQueueIsolateManager = getBinding(); + final _imagePaths = Get.find(); + final _appToast = Get.find(); + + final listSendingEmailController = ScrollController(); + + final selectionState = Rx(SelectMode.INACTIVE); + + SendingQueueController( + this._deleteMultipleSendingEmailInteractor, + this._updateMultipleSendingEmailInteractor, + this._updateSendingEmailInteractor, + this._deleteSendingEmailInteractor, + this._getStoredSendingEmailInteractor, + ); + + @override + void onInit() { + super.onInit(); + _sendingQueueIsolateManager?.initial( + onData: _handleSendingQueueEvent, + onError: (error, stackTrace) { + logError('SendingQueueController::onInit():onError:error: $error | stackTrace: $stackTrace'); + } + ); + } + + void _handleSendingQueueEvent(Object? event) async { + log('SendingQueueController::_handleSendingQueueEvent():event: $event'); + try { + if (event is String) { + final tupleKey = TupleKey.fromString(event); + log('SendingQueueController::_handleSendingQueueEvent():tupleKey: $tupleKey'); + if (tupleKey.parts.length >= 2) { + final sendingId = tupleKey.parts[0]; + final sendingState = SendingState.values.firstWhere((state) => state.name == tupleKey.parts[1]); + if (tupleKey.parts.length >= 4) { + final accountId = AccountId(Id(tupleKey.parts[2])); + final userName = UserName(tupleKey.parts[3]); + _updatingSendingStateAction( + sendingId: sendingId, + newState: sendingState, + accountId: accountId, + userName: userName + ); + } else { + _updatingSendingStateAction(sendingId: sendingId, newState: sendingState); + } + } + } + } catch (e) { + logError('SendingQueueController::_handleSendingQueueEvent(): EXCEPTION: $e'); + } + } + + void _updatingSendingStateAction({ + required String sendingId, + required SendingState newState, + AccountId? accountId, + UserName? userName + }) async { + log('SendingQueueController::_updatingSendingStateAction():sendingId: $sendingId | newState: $newState'); + switch(newState) { + case SendingState.waiting: + case SendingState.running: + final listSendingEmails = dashboardController.listSendingEmails + .map((sendingEmail) => sendingEmail.sendingId == sendingId + ? sendingEmail.updatingSendingState(newState) + : sendingEmail) + .toList(); + + dashboardController.listSendingEmails.value = listSendingEmails; + break; + case SendingState.canceled: + case SendingState.error: + if (accountId != null && userName != null) { + final matchedSendingEmail = dashboardController.listSendingEmails.firstWhereOrNull((sendingEmail) => sendingEmail.sendingId == sendingId); + if (matchedSendingEmail != null) { + _updateSendingEmailAction( + newSendingEmail: matchedSendingEmail.updatingSendingState(newState), + accountId: accountId, + userName: userName + ); + } else { + _getStoredSendingEmailAction(sendingId, accountId, userName, newState); + } + } + break; + case SendingState.success: + if (accountId != null && userName != null) { + _deleteSendingEmailAction(sendingId, accountId, userName); + } + break; + } + } + + void handleOnLongPressAction(SendingEmail sendingEmail) { + final newListSendingEmail = dashboardController.listSendingEmails.toggleSelection(sendingEmailSelected: sendingEmail); + dashboardController.listSendingEmails.value = newListSendingEmail; + + selectionState.value = newListSendingEmail.isAllUnSelected() + ? SelectMode.INACTIVE + : SelectMode.ACTIVE; + } + + void toggleSelectionSendingEmail(SendingEmail sendingEmail) { + final newListSendingEmail = dashboardController.listSendingEmails.toggleSelection(sendingEmailSelected: sendingEmail); + dashboardController.listSendingEmails.value = newListSendingEmail; + + selectionState.value = newListSendingEmail.isAllUnSelected() + ? SelectMode.INACTIVE + : SelectMode.ACTIVE; + } + + bool get isAllUnSelected => dashboardController.listSendingEmails.isAllUnSelected(); + + bool get isConnectedNetwork => _networkConnectionController.isNetworkConnectionAvailable() == true; + + void refreshSendingQueue() { + dashboardController.getAllSendingEmails(); + } + + void openMailboxMenu() { + dashboardController.openMailboxMenuDrawer(); + } + + void disableSelectionMode() { + final newListSendingEmail = dashboardController.listSendingEmails.unAllSelected(); + dashboardController.listSendingEmails.value = newListSendingEmail; + selectionState.value = SelectMode.INACTIVE; + } + + void handleSendingEmailActionType( + BuildContext context, + SendingEmailActionType actionType, + List listSendingEmails + ) { + switch(actionType) { + case SendingEmailActionType.delete: + _deleteListSendingEmailAction(context, listSendingEmails); + break; + case SendingEmailActionType.edit: + if (!isConnectedNetwork) { + _editSendingEmailAction(listSendingEmails.first); + } + break; + case SendingEmailActionType.create: + break; + case SendingEmailActionType.resend: + _resendSendingEmailAction(listSendingEmails); + break; + } + } + + void _deleteListSendingEmailAction(BuildContext context, List listSendingEmails) { + showConfirmDialogAction( + context, + AppLocalizations.of(context).messageDialogDeleteSendingEmail, + AppLocalizations.of(currentContext!).delete, + title: AppLocalizations.of(currentContext!).deleteOfflineEmail, + icon: SvgPicture.asset(_imagePaths.icDeleteDialogRecipients), + alignCenter: true, + messageStyle: const TextStyle( + color: AppColor.colorTitleSendingItem, + fontSize: 15, + fontWeight: FontWeight.normal + ), + titleStyle: const TextStyle( + color: AppColor.colorDeletePermanentlyButton, + fontSize: 20, + fontWeight: FontWeight.bold + ), + actionButtonColor: AppColor.colorDeletePermanentlyButton, + actionStyle: const TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.w500 + ), + cancelStyle: const TextStyle( + color: AppColor.colorDeletePermanentlyButton, + fontSize: 17, + fontWeight: FontWeight.w500 + ), + onConfirmAction: () => _handleDeleteListSendingEmail(listSendingEmails), + ); + } + + void _handleDeleteListSendingEmail(List listSendingEmails) async { + disableSelectionMode(); + + final accountId = dashboardController.accountId.value; + final session = dashboardController.sessionCurrent; + if (accountId != null && session != null) { + consumeState( + _deleteMultipleSendingEmailInteractor.execute( + accountId, + session.username, + listSendingEmails.sendingIds + ) + ); + } + } + + void _handleDeleteListSendingEmailSuccess(DeleteMultipleSendingEmailSuccess success) async { + await Future.wait(success.sendingIds.map(WorkManagerController().cancelByUniqueId)); + + if (currentContext != null && currentOverlayContext != null) { + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).messageHaveBeenDeletedSuccessfully); + } + + refreshSendingQueue(); + } + + void _editSendingEmailAction(SendingEmail sendingEmail) { + disableSelectionMode(); + dashboardController.goToComposer(ComposerArguments.fromSendingEmail(sendingEmail)); + } + + void _resendSendingEmailAction(List listSendingEmails) async { + disableSelectionMode(); + + final accountId = dashboardController.accountId.value; + final session = dashboardController.sessionCurrent; + + if (accountId != null && session != null) { + consumeState( + _updateMultipleSendingEmailInteractor.execute( + accountId, + session.username, + listSendingEmails.toSendingStateWaiting() + ) + ); + } + } + + void _handleResendSendingEmailSuccess(List newListSendingEmails) async { + await Future.forEach(newListSendingEmails, (sendingEmail) async { + await WorkManagerController().cancelByUniqueId(sendingEmail.sendingId); + dashboardController.addSendingEmailToSendingQueue(sendingEmail); + }); + + if (currentContext != null && currentOverlayContext != null) { + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).messagesHaveBeenResent); + } + refreshSendingQueue(); + } + + void _updateSendingEmailAction({ + required SendingEmail newSendingEmail, + required AccountId accountId, + required UserName userName + }) { + consumeState(_updateSendingEmailInteractor.execute(accountId, userName, newSendingEmail)); + } + + void _deleteSendingEmailAction(String sendingId, AccountId accountId, UserName userName) { + consumeState(_deleteSendingEmailInteractor.execute(accountId, userName, sendingId)); + } + + void _handleUpdateSendingEmailSuccess(UpdateSendingEmailSuccess success) async { + await WorkManagerController().cancelByUniqueId(success.newSendingEmail.sendingId); + refreshSendingQueue(); + } + + void _getStoredSendingEmailAction( + String sendingId, + AccountId accountId, + UserName userName, + SendingState sendingState + ) { + consumeState( + _getStoredSendingEmailInteractor.execute( + accountId, + userName, + sendingId, + sendingState + ) + ); + } + + Future backButtonPressedCallbackAction(BuildContext context) async { + if (PlatformInfo.isMobile) { + dashboardController.openDefaultMailbox(); + } + return false; + } + + @override + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is DeleteMultipleSendingEmailSuccess) { + _handleDeleteListSendingEmailSuccess(success); + } else if (success is UpdateMultipleSendingEmailAllSuccess) { + _handleResendSendingEmailSuccess(success.newSendingEmails); + } else if (success is UpdateMultipleSendingEmailHasSomeSuccess) { + _handleResendSendingEmailSuccess(success.newSendingEmails); + } else if (success is UpdateSendingEmailSuccess) { + _handleUpdateSendingEmailSuccess(success); + } else if (success is DeleteSendingEmailSuccess) { + refreshSendingQueue(); + } else if (success is GetStoredSendingEmailSuccess) { + _updateSendingEmailAction( + newSendingEmail: success.sendingEmail.updatingSendingState(success.sendingState), + accountId: success.accountId, + userName: success.userName + ); + } + } + + @override + void onClose() { + listSendingEmailController.dispose(); + _sendingQueueIsolateManager?.release(); + super.onClose(); + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/sending_queue_view.dart b/lib/features/sending_queue/presentation/sending_queue_view.dart new file mode 100644 index 0000000000..c34834b894 --- /dev/null +++ b/lib/features/sending_queue/presentation/sending_queue_view.dart @@ -0,0 +1,123 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/base/widget/compose_floating_button.dart'; +import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/extensions/list_sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/sending_queue_controller.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/widgets/app_bar_sending_queue_widget.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/widgets/banner_message_sending_queue_widget.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/widgets/bottom_bar_sending_queue_widget.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/widgets/sending_email_tile_widget.dart'; + +class SendingQueueView extends GetWidget with AppLoaderMixin { + + const SendingQueueView({super.key}); + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () => controller.backButtonPressedCallbackAction.call(context), + child: Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Column( + children: [ + Obx(() { + return AppBarSendingQueueWidget( + listSendingEmailSelected: controller.dashboardController.listSendingEmails.listSelected(), + onOpenMailboxMenu: controller.openMailboxMenu, + onBackAction: controller.disableSelectionMode, + selectMode: controller.selectionState.value, + ); + }), + const Divider(color: AppColor.colorDividerComposer, height: 1), + Obx(() { + if (!controller.isConnectedNetwork) { + return const BannerMessageSendingQueueWidget(); + } else { + return const SizedBox.shrink(); + } + }), + Expanded(child: _buildListSendingEmails(context)), + Obx(() { + if (controller.isAllUnSelected) { + return const SizedBox.shrink(); + } else { + return BottomBarSendingQueueWidget( + listSendingEmailSelected: controller.dashboardController.listSendingEmails.listSelected(), + isConnectedNetwork: controller.isConnectedNetwork, + onHandleSendingEmailActionType: (actionType, listSendingEmails) => controller.handleSendingEmailActionType(context, actionType, listSendingEmails), + ); + } + }), + ] + ), + ), + floatingActionButton: _buildFloatingButtonCompose(), + ), + ); + } + + Widget _buildListSendingEmails(BuildContext context) { + return Obx(() { + if (controller.selectionState.value == SelectMode.INACTIVE) { + return RefreshIndicator( + color: AppColor.primaryColor, + onRefresh: () async => controller.refreshSendingQueue(), + child: _buildListViewItemSendingEmails()); + } else { + return _buildListViewItemSendingEmails(); + } + }); + } + + Widget _buildListViewItemSendingEmails() { + return Obx(() { + final listSendingEmails = controller.dashboardController.listSendingEmails; + if (listSendingEmails.isNotEmpty) { + return ListView.builder( + controller: controller.listSendingEmailController, + physics: const AlwaysScrollableScrollPhysics(), // Trigger Refresh To pull + itemCount: listSendingEmails.length, + itemBuilder: (context, index) { + return SendingEmailTileWidget( + sendingEmail: listSendingEmails[index], + selectMode: controller.selectionState.value, + onLongPressAction: controller.handleOnLongPressAction, + onSelectLeadingAction: controller.toggleSelectionSendingEmail, + onTapAction: (actionType, sendingEmail) { + if (!controller.isConnectedNetwork && sendingEmail.isEditableSupported) { + controller.handleSendingEmailActionType(context, actionType, [sendingEmail]); + } + }); + } + ); + } else { + return const SizedBox.shrink(); + } + }); + } + + Widget _buildFloatingButtonCompose() { + return Obx(() { + if (controller.isAllUnSelected) { + return ComposeFloatingButton( + scrollController: controller.listSendingEmailController, + onTap: () => controller.dashboardController.goToComposer(ComposerArguments()) + ); + } else { + return Container( + padding: const EdgeInsets.only(bottom: 70), + child: ComposeFloatingButton( + scrollController: controller.listSendingEmailController, + onTap: () => controller.dashboardController.goToComposer(ComposerArguments()) + ), + ); + } + }); + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart b/lib/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart new file mode 100644 index 0000000000..a0356ecbd8 --- /dev/null +++ b/lib/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart @@ -0,0 +1,8 @@ + +import 'package:tmail_ui_user/features/base/isolate/isolate_manager.dart'; + +class SendingQueueIsolateManager extends IsolateManager { + + @override + String get isolateIdentityName => 'sending_queue_isolate'; +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/utils/sending_queue_utils.dart b/lib/features/sending_queue/presentation/utils/sending_queue_utils.dart new file mode 100644 index 0000000000..6525845627 --- /dev/null +++ b/lib/features/sending_queue/presentation/utils/sending_queue_utils.dart @@ -0,0 +1,38 @@ + +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/cupertino.dart'; + +class SendingQueueUtils { + + static EdgeInsets getPaddingAppBarByResponsiveSize(double width) { + if (ResponsiveUtils.isMatchedMobileWidth(width)) { + return const EdgeInsets.symmetric(horizontal: 10); + } else { + return const EdgeInsets.symmetric(horizontal: 24); + } + } + + static EdgeInsets getMarginBannerMessageByResponsiveSize(double width) { + if (ResponsiveUtils.isMatchedMobileWidth(width)) { + return const EdgeInsets.only(top: 16, left: 16, right: 16); + } else { + return const EdgeInsets.symmetric(horizontal: 24, vertical: 16); + } + } + + static EdgeInsets getPaddingItemListViewByResponsiveSize(double width) { + if (ResponsiveUtils.isMatchedMobileWidth(width)) { + return const EdgeInsets.symmetric(horizontal: 16, vertical: 8); + } else { + return const EdgeInsets.symmetric(horizontal: 32, vertical: 8); + } + } + + static EdgeInsets getPaddingDividerListViewByResponsiveSize(double width) { + if (ResponsiveUtils.isMatchedMobileWidth(width)) { + return const EdgeInsets.only(left: 84, right: 8, top: 8); + } else { + return const EdgeInsets.only(left: 100, right: 24, top: 8); + } + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/widgets/app_bar_sending_queue_widget.dart b/lib/features/sending_queue/presentation/widgets/app_bar_sending_queue_widget.dart new file mode 100644 index 0000000000..a0971a6858 --- /dev/null +++ b/lib/features/sending_queue/presentation/widgets/app_bar_sending_queue_widget.dart @@ -0,0 +1,105 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_utils.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class AppBarSendingQueueWidget extends StatelessWidget { + + final VoidCallback? onBackAction; + final VoidCallback? onOpenMailboxMenu; + final SelectMode selectMode; + final List listSendingEmailSelected; + + const AppBarSendingQueueWidget({ + super.key, + required this.listSendingEmailSelected, + this.onBackAction, + this.onOpenMailboxMenu, + this.selectMode = SelectMode.INACTIVE + }); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + + return LayoutBuilder(builder: (context, constraints) { + return Container( + height: 52, + color: Colors.white, + padding: SendingQueueUtils.getPaddingAppBarByResponsiveSize(constraints.maxWidth), + child: Row( + children: [ + if (selectMode == SelectMode.INACTIVE) + buildIconWeb( + icon: SvgPicture.asset( + imagePaths.icMenuMailbox, + colorFilter: AppColor.colorTextButton.asFilter(), + fit: BoxFit.fill + ), + tooltip: AppLocalizations.of(context).openMailboxMenu, + onTap: onOpenMailboxMenu + ) + else + Padding( + padding: const EdgeInsets.only(left: 8), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(15), + child: InkWell( + onTap: onBackAction, + borderRadius: BorderRadius.circular(15), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SvgPicture.asset( + DirectionUtils.isDirectionRTLByLanguage(context) ? imagePaths.icCollapseFolder : imagePaths.icBack, + colorFilter: AppColor.colorTextButton.asFilter(), + fit: BoxFit.fill + ), + const SizedBox(width: 8), + Text( + listSendingEmailSelected.length.toString(), + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: const TextStyle( + fontSize: 17, + color: AppColor.colorTextButton + ) + ) + ] + ), + ), + ), + ), + ), + Expanded(child: Text( + AppLocalizations.of(context).sendingQueue, + maxLines: 1, + textAlign: TextAlign.center, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: const TextStyle( + fontSize: 21, + color: Colors.black, + fontWeight: FontWeight.bold + ) + )), + const SizedBox(width: 50) + ], + ), + ); + }); + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/widgets/banner_message_sending_queue_widget.dart b/lib/features/sending_queue/presentation/widgets/banner_message_sending_queue_widget.dart new file mode 100644 index 0000000000..7d9e11db7d --- /dev/null +++ b/lib/features/sending_queue/presentation/widgets/banner_message_sending_queue_widget.dart @@ -0,0 +1,50 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_utils.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class BannerMessageSendingQueueWidget extends StatelessWidget { + + const BannerMessageSendingQueueWidget({super.key}); + + @override + Widget build(BuildContext context) { + final imagePath = getBinding(); + + return LayoutBuilder(builder: (context, constraints) { + return Container( + margin: SendingQueueUtils.getMarginBannerMessageByResponsiveSize(constraints.maxWidth), + padding: const EdgeInsets.all(16), + decoration: const BoxDecoration( + color: AppColor.colorBannerMessageSendingQueue, + borderRadius: BorderRadius.all(Radius.circular(8)) + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + imagePath!.icMailboxSendingQueue, + fit: BoxFit.fill, + width: 20, + height: 20, + colorFilter: AppColor.colorTitleSendingItem.asFilter(), + ), + const SizedBox(width: 8), + Expanded(child: Text( + AppLocalizations.of(context).bannerMessageSendingQueueView, + style: const TextStyle( + fontSize: 15, + color: Colors.black, + fontWeight: FontWeight.normal + ) + )) + ] + ), + ); + }); + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/widgets/bottom_bar_sending_queue_widget.dart b/lib/features/sending_queue/presentation/widgets/bottom_bar_sending_queue_widget.dart new file mode 100644 index 0000000000..997a067f2a --- /dev/null +++ b/lib/features/sending_queue/presentation/widgets/bottom_bar_sending_queue_widget.dart @@ -0,0 +1,113 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/base/state/button_state.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/extensions/list_sending_email_extension.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; + +typedef OnHandleSendingEmailActionType = void Function(SendingEmailActionType, List); + +class BottomBarSendingQueueWidget extends StatelessWidget { + + final List listSendingEmailSelected; + final OnHandleSendingEmailActionType? onHandleSendingEmailActionType; + final bool isConnectedNetwork; + + const BottomBarSendingQueueWidget({ + super.key, + required this.listSendingEmailSelected, + this.onHandleSendingEmailActionType, + this.isConnectedNetwork = true + }); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + final responsiveUtils = Get.find(); + + return Container( + decoration: const BoxDecoration( + border: Border(top: BorderSide( + color: AppColor.colorDividerHorizontal, + width: 0.5, + )), + color: Colors.white + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: TMailButtonWidget( + key: Key(SendingEmailActionType.edit.getButtonKey()), + text: SendingEmailActionType.edit.getButtonTitle(context), + icon: imagePaths.icEdit, + borderRadius: 0, + backgroundColor: Colors.transparent, + iconColor: SendingEmailActionType.edit.getButtonIconColor(_isEditable ? ButtonState.enabled : ButtonState.disabled), + textStyle: TextStyle( + fontSize: 12, + color: SendingEmailActionType.edit.getButtonTitleColor(_isEditable ? ButtonState.enabled : ButtonState.disabled) + ), + verticalDirection: responsiveUtils.isPortraitMobile(context), + onTapActionCallback: () { + if (_isEditable) { + onHandleSendingEmailActionType?.call( + SendingEmailActionType.edit, + listSendingEmailSelected + ); + } + }, + ), + ), + Expanded( + child: TMailButtonWidget( + key: Key(SendingEmailActionType.resend.getButtonKey()), + text: SendingEmailActionType.resend.getButtonTitle(context), + icon: imagePaths.icRefresh, + borderRadius: 0, + backgroundColor: Colors.transparent, + iconColor: SendingEmailActionType.resend.getButtonIconColor(_isCanResend ? ButtonState.enabled : ButtonState.disabled), + textStyle: TextStyle( + fontSize: 12, + color: SendingEmailActionType.resend.getButtonTitleColor(_isCanResend ? ButtonState.enabled : ButtonState.disabled) + ), + verticalDirection: responsiveUtils.isPortraitMobile(context), + onTapActionCallback: () { + if (_isCanResend) { + onHandleSendingEmailActionType?.call(SendingEmailActionType.resend, listSendingEmailSelected); + } + }, + ), + ), + Expanded( + child: TMailButtonWidget( + key: Key(SendingEmailActionType.delete.getButtonKey()), + text: SendingEmailActionType.delete.getButtonTitle(context), + icon: imagePaths.icDeleteComposer, + borderRadius: 0, + backgroundColor: Colors.transparent, + iconColor: SendingEmailActionType.delete.getButtonIconColor(ButtonState.enabled), + textStyle: TextStyle( + fontSize: 12, + color: SendingEmailActionType.delete.getButtonTitleColor(ButtonState.enabled) + ), + verticalDirection: responsiveUtils.isPortraitMobile(context), + onTapActionCallback: () => onHandleSendingEmailActionType?.call(SendingEmailActionType.delete, listSendingEmailSelected), + ), + ), + ], + ), + ); + } + + bool get _isEditable => !isConnectedNetwork && + listSendingEmailSelected.length == 1 && + listSendingEmailSelected.first.isEditableSupported; + + bool get _isCanResend => listSendingEmailSelected.isAllSendingStateError(); +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/widgets/sending_email_tile_widget.dart b/lib/features/sending_queue/presentation/widgets/sending_email_tile_widget.dart new file mode 100644 index 0000000000..32e6859680 --- /dev/null +++ b/lib/features/sending_queue/presentation/widgets/sending_email_tile_widget.dart @@ -0,0 +1,166 @@ +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/text/text_overflow_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/sending_queue/domain/model/sending_email.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/model/sending_email_action_type.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_utils.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/widgets/sending_state_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnLongPressSendingEmailItemAction = void Function(SendingEmail); +typedef OnSelectSendingEmailItemAction = void Function(SendingEmailActionType, SendingEmail); +typedef OnSelectLeadingSendingEmailItemAction = void Function(SendingEmail); + +class SendingEmailTileWidget extends StatelessWidget { + + final SendingEmail sendingEmail; + final SelectMode selectMode; + final OnLongPressSendingEmailItemAction? onLongPressAction; + final OnSelectSendingEmailItemAction? onTapAction; + final OnSelectLeadingSendingEmailItemAction? onSelectLeadingAction; + + const SendingEmailTileWidget({ + super.key, + required this.sendingEmail, + required this.selectMode, + this.onLongPressAction, + this.onTapAction, + this.onSelectLeadingAction, + }); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + + return LayoutBuilder(builder: (context, constraints) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => onTapAction?.call(SendingEmailActionType.edit, sendingEmail), + onLongPress: () => onLongPressAction?.call(sendingEmail), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: SendingQueueUtils.getPaddingItemListViewByResponsiveSize(constraints.maxWidth), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (selectMode == SelectMode.ACTIVE) + GestureDetector( + child: Container( + width: 60, + height: 60, + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(30)) + ), + alignment: Alignment.center, + child: SvgPicture.asset( + sendingEmail.isSelected + ? imagePaths.icSelected + : imagePaths.icUnSelected, + fit: BoxFit.fill, + width: 24, + height: 24 + ), + ), + onTap: () => onSelectLeadingAction?.call(sendingEmail), + ) + else + SvgPicture.asset( + sendingEmail.presentationEmail.numberOfAllEmailAddress() == 1 + ? sendingEmail.sendingState.getAvatarPersonal(imagePaths) + : sendingEmail.sendingState.getAvatarGroup(imagePaths), + fit: BoxFit.fill, + width: 60, + height: 60, + ), + const SizedBox(width: 8), + Expanded(child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Expanded(child: TextOverflowBuilder( + AppLocalizations.of(context).titleRecipientSendingEmail(sendingEmail.presentationEmail.recipientsName()), + style: TextStyle( + fontSize: 15, + color: sendingEmail.sendingState.getTitleSendingEmailItemColor(), + fontWeight: FontWeight.w600 + ) + )), + if (sendingEmail.email.attachments != null && sendingEmail.email.attachments!.isNotEmpty) + Padding( + padding: const EdgeInsets.only(left: 8), + child: SvgPicture.asset( + imagePaths.icAttachment, + width: 20, + height: 20, + fit: BoxFit.fill + ) + ), + Padding( + padding: const EdgeInsets.only(left: 8), + child: Text( + sendingEmail.getCreateTimeAt(Localizations.localeOf(context).toLanguageTag()), + maxLines: 1, + softWrap: CommonTextStyle.defaultSoftWrap, + overflow: CommonTextStyle.defaultTextOverFlow, + style: TextStyle( + fontSize: 13, + color: sendingEmail.sendingState.getTitleSendingEmailItemColor(), + fontWeight: FontWeight.normal + ) + ) + ), + if (!ResponsiveUtils.isMatchedMobileWidth(constraints.maxWidth)) + Container( + margin: const EdgeInsets.only(left: 8), + width: 120, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Spacer(), + SendingStateWidget( + sendingState: sendingEmail.sendingState, + constraints: const BoxConstraints(maxWidth: 80), + ) + ] + ) + ) + ]), + const SizedBox(height: 8), + TextOverflowBuilder( + sendingEmail.presentationEmail.getEmailTitle(), + style: TextStyle( + fontSize: 13, + color: sendingEmail.sendingState.getSubTitleSendingEmailItemColor(), + fontWeight: FontWeight.normal + ) + ), + if (ResponsiveUtils.isMatchedMobileWidth(constraints.maxWidth)) + Padding( + padding: const EdgeInsets.only(top: 8), + child: SendingStateWidget(sendingState: sendingEmail.sendingState)) + ] + )) + ]), + ), + Padding( + padding: SendingQueueUtils.getPaddingDividerListViewByResponsiveSize(constraints.maxWidth), + child: const Divider(color: AppColor.lineItemListColor, height: 1), + ) + ], + ), + ), + ); + }); + } +} \ No newline at end of file diff --git a/lib/features/sending_queue/presentation/widgets/sending_state_widget.dart b/lib/features/sending_queue/presentation/widgets/sending_state_widget.dart new file mode 100644 index 0000000000..72bfa55d65 --- /dev/null +++ b/lib/features/sending_queue/presentation/widgets/sending_state_widget.dart @@ -0,0 +1,71 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tmail_ui_user/features/offline_mode/model/sending_state.dart'; +import 'package:tmail_ui_user/main/routes/route_navigation.dart'; + +class SendingStateWidget extends StatelessWidget { + + final SendingState sendingState; + final BoxConstraints? constraints; + + const SendingStateWidget({ + super.key, + required this.sendingState, + this.constraints + }); + + @override + Widget build(BuildContext context) { + final imagePath = getBinding(); + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + decoration: BoxDecoration( + color: sendingState.getBackgroundColor(), + borderRadius: const BorderRadius.all(Radius.circular(16)) + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + sendingState.getIcon(imagePath!), + fit: BoxFit.fill, + width: 20, + height: 20 + ), + const SizedBox(width: 4), + if (constraints != null) + ConstrainedBox( + constraints: constraints!, + child: Text( + sendingState.getTitle(context), + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: TextStyle( + fontSize: 15, + color: sendingState.getTitleColor(), + fontWeight: FontWeight.normal + ) + ), + ) + else + Text( + sendingState.getTitle(context), + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: TextStyle( + fontSize: 15, + color: sendingState.getTitleColor(), + fontWeight: FontWeight.normal + ) + ) + ] + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/session/data/datasource/session_datasource.dart b/lib/features/session/data/datasource/session_datasource.dart index f89c9e42dc..4fe998566d 100644 --- a/lib/features/session/data/datasource/session_datasource.dart +++ b/lib/features/session/data/datasource/session_datasource.dart @@ -2,4 +2,8 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; abstract class SessionDataSource { Future getSession(); + + Future storeSession(Session session); + + Future getStoredSession(); } \ No newline at end of file diff --git a/lib/features/session/data/datasource_impl/hive_session_datasource_impl.dart b/lib/features/session/data/datasource_impl/hive_session_datasource_impl.dart new file mode 100644 index 0000000000..07275b624e --- /dev/null +++ b/lib/features/session/data/datasource_impl/hive_session_datasource_impl.dart @@ -0,0 +1,40 @@ +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/caching/clients/session_hive_cache_client.dart'; +import 'package:tmail_ui_user/features/session/data/datasource/session_datasource.dart'; +import 'package:tmail_ui_user/features/session/data/exceptions/session_exceptions.dart'; +import 'package:tmail_ui_user/features/session/data/extensions/session_hive_obj_extension.dart'; +import 'package:tmail_ui_user/features/session/data/model/session_hive_obj.dart'; +import 'package:tmail_ui_user/features/session/domain/extensions/session_extensions.dart'; +import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; + +class HiveSessionDataSourceImpl extends SessionDataSource { + + final SessionHiveCacheClient _sessionHiveCacheClient; + final ExceptionThrower _exceptionThrower; + + HiveSessionDataSourceImpl(this._sessionHiveCacheClient, this._exceptionThrower); + + @override + Future getSession() { + throw UnimplementedError(); + } + + @override + Future storeSession(Session session) { + return Future.sync(() async { + return _sessionHiveCacheClient.insertItem(SessionHiveObj.keyValue, session.toHiveObj()); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future getStoredSession() { + return Future.sync(() async { + final sessionHiveObj = await _sessionHiveCacheClient.getItem(SessionHiveObj.keyValue); + if (sessionHiveObj != null) { + return sessionHiveObj.toSession(); + } else { + throw NotFoundSessionHiveObject(); + } + }).catchError(_exceptionThrower.throwException); + } +} \ No newline at end of file diff --git a/lib/features/session/data/datasource_impl/session_datasource_impl.dart b/lib/features/session/data/datasource_impl/session_datasource_impl.dart index 5b68cfc079..41777496f1 100644 --- a/lib/features/session/data/datasource_impl/session_datasource_impl.dart +++ b/lib/features/session/data/datasource_impl/session_datasource_impl.dart @@ -14,8 +14,16 @@ class SessionDataSourceImpl extends SessionDataSource { Future getSession() { return Future.sync(() async { return await _sessionAPI.getSession(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); + } + + @override + Future storeSession(Session session) { + throw UnimplementedError(); + } + + @override + Future getStoredSession() { + throw UnimplementedError(); } } \ No newline at end of file diff --git a/lib/features/session/data/exceptions/session_exceptions.dart b/lib/features/session/data/exceptions/session_exceptions.dart new file mode 100644 index 0000000000..705ee3ceea --- /dev/null +++ b/lib/features/session/data/exceptions/session_exceptions.dart @@ -0,0 +1,2 @@ + +class NotFoundSessionHiveObject implements Exception {} \ No newline at end of file diff --git a/lib/features/session/data/extensions/session_hive_obj_extension.dart b/lib/features/session/data/extensions/session_hive_obj_extension.dart new file mode 100644 index 0000000000..2a8c0da139 --- /dev/null +++ b/lib/features/session/data/extensions/session_hive_obj_extension.dart @@ -0,0 +1,9 @@ + +import 'dart:convert'; + +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/session/data/model/session_hive_obj.dart'; + +extension SessionHiveObjExtension on SessionHiveObj { + Session toSession() => Session.fromJson(jsonDecode(value)); +} \ No newline at end of file diff --git a/lib/features/session/data/model/session_hive_obj.dart b/lib/features/session/data/model/session_hive_obj.dart new file mode 100644 index 0000000000..4d207e136b --- /dev/null +++ b/lib/features/session/data/model/session_hive_obj.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; +import 'package:hive/hive.dart'; +import 'package:tmail_ui_user/features/caching/utils/caching_constants.dart'; + +part 'session_hive_obj.g.dart'; + +@HiveType(typeId: CachingConstants.typeIdSessionHiveObj) +class SessionHiveObj extends HiveObject with EquatableMixin { + + static const String keyValue = 'session'; + + @HiveField(0) + final String value; + + SessionHiveObj({ + required this.value + }); + + @override + List get props => [value]; +} \ No newline at end of file diff --git a/lib/features/session/data/repository/session_repository_impl.dart b/lib/features/session/data/repository/session_repository_impl.dart index 39ec4df2ed..3613f4cd3f 100644 --- a/lib/features/session/data/repository/session_repository_impl.dart +++ b/lib/features/session/data/repository/session_repository_impl.dart @@ -1,15 +1,26 @@ +import 'package:core/data/model/source_type/data_source_type.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/session/data/datasource/session_datasource.dart'; import 'package:tmail_ui_user/features/session/domain/repository/session_repository.dart'; class SessionRepositoryImpl extends SessionRepository { - final SessionDataSource sessionDataSource; + final Map sessionDataSource; SessionRepositoryImpl(this.sessionDataSource); @override Future getSession() { - return sessionDataSource.getSession(); + return sessionDataSource[DataSourceType.network]!.getSession(); + } + + @override + Future storeSession(Session session) { + return sessionDataSource[DataSourceType.hiveCache]!.storeSession(session); + } + + @override + Future getStoredSession() { + return sessionDataSource[DataSourceType.hiveCache]!.getStoredSession(); } } \ No newline at end of file diff --git a/lib/features/session/domain/converter/capability_properties_converter.dart b/lib/features/session/domain/converter/capability_properties_converter.dart new file mode 100644 index 0000000000..d2e162d934 --- /dev/null +++ b/lib/features/session/domain/converter/capability_properties_converter.dart @@ -0,0 +1,34 @@ +import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart'; +import 'package:jmap_dart_client/jmap/core/capability/core_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/default_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/empty_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/mail_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/mdn_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/submission_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/vacation_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/websocket_capability.dart'; + +class CapabilityPropertiesConverter { + + Map? toJson(CapabilityProperties properties) { + if (properties is CoreCapability) { + return properties.toJson(); + } else if (properties is MailCapability) { + return properties.toJson(); + } else if (properties is SubmissionCapability) { + return properties.toJson(); + } else if (properties is VacationCapability) { + return properties.toJson(); + } else if (properties is WebSocketCapability) { + return properties.toJson(); + } else if (properties is MdnCapability) { + return properties.toJson(); + } else if (properties is DefaultCapability) { + return properties.properties; + } else if (properties is EmptyCapability) { + return properties.toJson(); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/lib/features/session/domain/converter/session_account_converter.dart b/lib/features/session/domain/converter/session_account_converter.dart new file mode 100644 index 0000000000..9dbed1d940 --- /dev/null +++ b/lib/features/session/domain/converter/session_account_converter.dart @@ -0,0 +1,14 @@ +import 'package:jmap_dart_client/http/converter/account_id_converter.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/account/account.dart'; +import 'package:tmail_ui_user/features/session/domain/extensions/session_account_extension.dart'; + +class SessionAccountConverter { + + MapEntry convertToMapEntry(AccountId accountId, Account account) { + return MapEntry( + const AccountIdConverter().toJson(accountId), + account.toJson() + ); + } +} \ No newline at end of file diff --git a/lib/features/session/domain/converter/session_capabilities_converter.dart b/lib/features/session/domain/converter/session_capabilities_converter.dart new file mode 100644 index 0000000000..e458b0204e --- /dev/null +++ b/lib/features/session/domain/converter/session_capabilities_converter.dart @@ -0,0 +1,14 @@ +import 'package:jmap_dart_client/http/converter/capability_identifier_converter.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart'; +import 'package:tmail_ui_user/features/session/domain/converter/capability_properties_converter.dart'; + +class SessionCapabilitiesConverter { + + MapEntry convertToMapEntry(CapabilityIdentifier identifier, CapabilityProperties properties) { + return MapEntry( + const CapabilityIdentifierConverter().toJson(identifier), + CapabilityPropertiesConverter().toJson(properties) + ); + } +} \ No newline at end of file diff --git a/lib/features/session/domain/converter/session_primary_account_converter.dart b/lib/features/session/domain/converter/session_primary_account_converter.dart new file mode 100644 index 0000000000..572e5c0883 --- /dev/null +++ b/lib/features/session/domain/converter/session_primary_account_converter.dart @@ -0,0 +1,14 @@ +import 'package:jmap_dart_client/http/converter/account_id_converter.dart'; +import 'package:jmap_dart_client/http/converter/capability_identifier_converter.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; + +class SessionPrimaryAccountConverter { + + MapEntry convertToMapEntry(CapabilityIdentifier identifier, AccountId accountId) { + return MapEntry( + const CapabilityIdentifierConverter().toJson(identifier), + const AccountIdConverter().toJson(accountId) + ); + } +} \ No newline at end of file diff --git a/lib/features/session/domain/extensions/session_account_extension.dart b/lib/features/session/domain/extensions/session_account_extension.dart new file mode 100644 index 0000000000..b4eba81fc8 --- /dev/null +++ b/lib/features/session/domain/extensions/session_account_extension.dart @@ -0,0 +1,24 @@ + +import 'package:jmap_dart_client/http/converter/account_name_converter.dart'; +import 'package:jmap_dart_client/jmap/core/account/account.dart'; +import 'package:tmail_ui_user/features/session/domain/converter/session_capabilities_converter.dart'; + +extension SessionAccountExtension on Account { + + Map toJson() { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('name', const AccountNameConverter().toJson(name)); + writeNotNull('isPersonal', isPersonal); + writeNotNull('isReadOnly', isReadOnly); + writeNotNull('accountCapabilities', accountCapabilities.map((key, value) => SessionCapabilitiesConverter().convertToMapEntry(key, value))); + + return val; + } +} \ No newline at end of file diff --git a/lib/features/session/domain/extensions/session_extensions.dart b/lib/features/session/domain/extensions/session_extensions.dart new file mode 100644 index 0000000000..c1363e75b9 --- /dev/null +++ b/lib/features/session/domain/extensions/session_extensions.dart @@ -0,0 +1,46 @@ + +import 'dart:convert'; + +import 'package:core/presentation/extensions/uri_extension.dart'; +import 'package:jmap_dart_client/http/converter/state_converter.dart'; +import 'package:jmap_dart_client/http/converter/user_name_converter.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/session/data/model/session_hive_obj.dart'; +import 'package:tmail_ui_user/features/session/domain/converter/session_account_converter.dart'; +import 'package:tmail_ui_user/features/session/domain/converter/session_capabilities_converter.dart'; +import 'package:tmail_ui_user/features/session/domain/converter/session_primary_account_converter.dart'; + +extension SessionExtensions on Session { + + Map toJson() { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('capabilities', capabilities.map((key, value) => SessionCapabilitiesConverter().convertToMapEntry(key, value))); + writeNotNull('accounts', accounts.map((key, value) => SessionAccountConverter().convertToMapEntry(key, value))); + writeNotNull('primaryAccounts', primaryAccounts.map((key, value) => SessionPrimaryAccountConverter().convertToMapEntry(key, value))); + writeNotNull('username', const UserNameConverter().toJson(username)); + writeNotNull('apiUrl', apiUrl.toString()); + writeNotNull('downloadUrl', downloadUrl.toString()); + writeNotNull('uploadUrl', uploadUrl.toString()); + writeNotNull('eventSourceUrl', eventSourceUrl.toString()); + writeNotNull('state', const StateConverter().toJson(state)); + + return val; + } + + SessionHiveObj toHiveObj() => SessionHiveObj(value: jsonEncode(toJson())); + + String getQualifiedApiUrl({String? baseUrl}) { + if (baseUrl != null) { + return apiUrl.toQualifiedUrl(baseUrl: Uri.parse(baseUrl)).toString(); + } else { + return apiUrl.toString(); + } + } +} \ No newline at end of file diff --git a/lib/features/session/domain/repository/session_repository.dart b/lib/features/session/domain/repository/session_repository.dart index 30a86774d6..c715fdba75 100644 --- a/lib/features/session/domain/repository/session_repository.dart +++ b/lib/features/session/domain/repository/session_repository.dart @@ -2,4 +2,8 @@ import 'package:jmap_dart_client/jmap/core/session/session.dart'; abstract class SessionRepository { Future getSession(); + + Future storeSession(Session session); + + Future getStoredSession(); } \ No newline at end of file diff --git a/lib/features/session/domain/state/get_session_state.dart b/lib/features/session/domain/state/get_session_state.dart index 511ee2b30d..180552ed96 100644 --- a/lib/features/session/domain/state/get_session_state.dart +++ b/lib/features/session/domain/state/get_session_state.dart @@ -1,6 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +class GetSessionLoading extends UIState {} + class GetSessionSuccess extends UIState { final Session session; @@ -11,10 +14,6 @@ class GetSessionSuccess extends UIState { } class GetSessionFailure extends FeatureFailure { - final dynamic exception; - - GetSessionFailure(this.exception); - @override - List get props => [exception]; + GetSessionFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/session/domain/state/get_stored_session_state.dart b/lib/features/session/domain/state/get_stored_session_state.dart new file mode 100644 index 0000000000..6d35970941 --- /dev/null +++ b/lib/features/session/domain/state/get_stored_session_state.dart @@ -0,0 +1,20 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; + +class GetStoredSessionLoading extends UIState {} + +class GetStoredSessionSuccess extends UIState { + + final Session session; + + GetStoredSessionSuccess(this.session); + + @override + List get props => [session]; +} + +class GetStoredSessionFailure extends FeatureFailure { + + GetStoredSessionFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/session/domain/state/store_session_state.dart b/lib/features/session/domain/state/store_session_state.dart new file mode 100644 index 0000000000..bb1a732a10 --- /dev/null +++ b/lib/features/session/domain/state/store_session_state.dart @@ -0,0 +1,11 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; + +class StoreSessionLoading extends UIState {} + +class StoreSessionSuccess extends UIState {} + +class StoreSessionFailure extends FeatureFailure { + + StoreSessionFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/session/domain/usecases/get_session_interactor.dart b/lib/features/session/domain/usecases/get_session_interactor.dart index 95d4973ece..c108a28be0 100644 --- a/lib/features/session/domain/usecases/get_session_interactor.dart +++ b/lib/features/session/domain/usecases/get_session_interactor.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; import 'package:tmail_ui_user/features/session/domain/repository/session_repository.dart'; import 'package:tmail_ui_user/features/session/domain/state/get_session_state.dart'; @@ -8,12 +9,13 @@ class GetSessionInteractor { GetSessionInteractor(this.sessionRepository); - Future> execute() async { + Stream> execute() async* { try { + yield Right(GetSessionLoading()); final session = await sessionRepository.getSession(); - return Right(GetSessionSuccess(session)); + yield Right(GetSessionSuccess(session)); } catch (e) { - return Left(GetSessionFailure(e)); + yield Left(GetSessionFailure(e)); } } } \ No newline at end of file diff --git a/lib/features/session/domain/usecases/get_stored_session_interactor.dart b/lib/features/session/domain/usecases/get_stored_session_interactor.dart new file mode 100644 index 0000000000..beacc16dc7 --- /dev/null +++ b/lib/features/session/domain/usecases/get_stored_session_interactor.dart @@ -0,0 +1,21 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/session/domain/repository/session_repository.dart'; +import 'package:tmail_ui_user/features/session/domain/state/get_stored_session_state.dart'; + +class GetStoredSessionInteractor { + final SessionRepository sessionRepository; + + GetStoredSessionInteractor(this.sessionRepository); + + Stream> execute() async* { + try { + yield Right(GetStoredSessionLoading()); + final session = await sessionRepository.getStoredSession(); + yield Right(GetStoredSessionSuccess(session)); + } catch (e) { + yield Left(GetStoredSessionFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/session/domain/usecases/store_session_interactor.dart b/lib/features/session/domain/usecases/store_session_interactor.dart new file mode 100644 index 0000000000..e0bd244f32 --- /dev/null +++ b/lib/features/session/domain/usecases/store_session_interactor.dart @@ -0,0 +1,22 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:tmail_ui_user/features/session/domain/repository/session_repository.dart'; +import 'package:tmail_ui_user/features/session/domain/state/store_session_state.dart'; + +class StoreSessionInteractor { + final SessionRepository sessionRepository; + + StoreSessionInteractor(this.sessionRepository); + + Stream> execute(Session session) async* { + try { + yield Right(StoreSessionLoading()); + await sessionRepository.storeSession(session); + yield Right(StoreSessionSuccess()); + } catch (e) { + yield Left(StoreSessionFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/session/presentation/session_controller.dart b/lib/features/session/presentation/session_controller.dart index 2c0802d89b..22aeb12967 100644 --- a/lib/features/session/presentation/session_controller.dart +++ b/lib/features/session/presentation/session_controller.dart @@ -1,19 +1,20 @@ import 'package:core/data/network/config/dynamic_url_interceptors.dart'; import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/base/reloadable/reloadable_controller.dart'; -import 'package:tmail_ui_user/features/caching/caching_manager.dart'; -import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/session/domain/extensions/session_extensions.dart'; import 'package:tmail_ui_user/features/session/domain/state/get_session_state.dart'; +import 'package:tmail_ui_user/features/session/domain/state/get_stored_session_state.dart'; import 'package:tmail_ui_user/features/session/domain/usecases/get_session_interactor.dart'; +import 'package:tmail_ui_user/features/session/domain/usecases/get_stored_session_interactor.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; @@ -23,28 +24,18 @@ import 'package:tmail_ui_user/main/routes/route_utils.dart'; class SessionController extends ReloadableController { final GetSessionInteractor _getSessionInteractor; - final DeleteCredentialInteractor _deleteCredentialInteractor; - final CachingManager _cachingManager; - final DeleteAuthorityOidcInteractor _deleteAuthorityOidcInteractor; - final AuthorizationInterceptors _authorizationInterceptors; final AppToast _appToast; final DynamicUrlInterceptors _dynamicUrlInterceptors; + final GetStoredSessionInteractor _getStoredSessionInteractor; SessionController( - LogoutOidcInteractor logoutOidcInteractor, - DeleteAuthorityOidcInteractor deleteAuthorityOidcInteractor, GetAuthenticatedAccountInteractor getAuthenticatedAccountInteractor, UpdateAuthenticationAccountInteractor updateAuthenticationAccountInteractor, this._getSessionInteractor, - this._deleteCredentialInteractor, - this._cachingManager, - this._deleteAuthorityOidcInteractor, - this._authorizationInterceptors, this._appToast, this._dynamicUrlInterceptors, + this._getStoredSessionInteractor, ) : super( - logoutOidcInteractor, - deleteAuthorityOidcInteractor, getAuthenticatedAccountInteractor, updateAuthenticationAccountInteractor ); @@ -60,66 +51,91 @@ class SessionController extends ReloadableController { } } + @override + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is GetSessionFailure) { + _handleSessionFailure(failure); + } + } + + @override + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetSessionSuccess) { + _goToMailboxDashBoard(success.session); + } else if (success is GetStoredSessionSuccess) { + _goToMailboxDashBoard(success.session); + } + } + + @override + void handleExceptionAction({Failure? failure, Exception? exception}) { + super.handleExceptionAction(failure: failure, exception: exception); + if (PlatformInfo.isMobile && failure is GetSessionFailure && exception is NoNetworkError) { + _handleGetStoredSession(); + } + } + @override void handleReloaded(Session session) { - pushAndPop( + popAndPush( RouteUtils.generateNavigationRoute(AppRoutes.dashboard, NavigationRouter()), arguments: session); } + void _handleGetStoredSession() { + log('SessionController::_handleGetStoredSession():'); + consumeState(_getStoredSessionInteractor.execute()); + } + void _getSession() async { - await _getSessionInteractor.execute() - .then((response) => response.fold( - (failure) { - _handleSessionFailure(failure); - _goToLogin(); - }, - (success) => success is GetSessionSuccess - ? _goToMailboxDashBoard(success) - : _goToLogin())); + consumeState(_getSessionInteractor.execute()); } void _handleSessionFailure(Failure failure) { + log('SessionController::_handleSessionFailure(): $failure'); if (failure is GetSessionFailure) { final sessionException = failure.exception; + var errorMessage = ''; if (_checkUrlError(sessionException) && currentContext != null) { - _appToast.showErrorToast(AppLocalizations.of(currentContext!).wrongUrlMessage); + errorMessage = AppLocalizations.of(currentContext!).wrongUrlMessage; + } else if (sessionException is BadCredentialsException && currentContext != null) { + errorMessage = AppLocalizations.of(currentContext!).badCredentials; + } else if (sessionException is ConnectionError && currentContext != null) { + errorMessage = AppLocalizations.of(currentContext!).connectionError; } else if (sessionException is UnknownError && currentContext != null) { - if (sessionException.message != null) { - _appToast.showErrorToast('[${sessionException.code}] ${sessionException.message}'); + if (sessionException.message != null && sessionException.code != null) { + errorMessage = '[${sessionException.code}] ${sessionException.message}'; + } else if (sessionException.message != null) { + errorMessage = sessionException.message!; } else { - _appToast.showErrorToast(AppLocalizations.of(currentContext!).unknownError); + errorMessage = AppLocalizations.of(currentContext!).unknownError; } } + + logError('SessionController::_handleSessionFailure():errorMessage: $errorMessage'); + if (errorMessage.isNotEmpty && currentOverlayContext != null) { + _appToast.showToastErrorMessage(currentOverlayContext!, errorMessage); + } } } bool _checkUrlError(dynamic sessionException) { - return sessionException is ConnectError || sessionException is BadGateway; - } - - void _goToLogin() async { - await Future.wait([ - _deleteCredentialInteractor.execute(), - _deleteAuthorityOidcInteractor.execute(), - _cachingManager.clearAll() - ]); - _authorizationInterceptors.clear(); - pushAndPopAll(AppRoutes.login); + return sessionException is ConnectionTimeout || sessionException is BadGateway || sessionException is SocketError; } - void _goToMailboxDashBoard(GetSessionSuccess success) { - final apiUrl = success.session.apiUrl.toString(); + void _goToMailboxDashBoard(Session session) { + final apiUrl = session.getQualifiedApiUrl(baseUrl: _dynamicUrlInterceptors.jmapUrl); + log('SessionController::_goToMailboxDashBoard():apiUrl: $apiUrl'); if (apiUrl.isNotEmpty) { _dynamicUrlInterceptors.changeBaseUrl(apiUrl); - pushAndPop( + popAndPush( RouteUtils.generateNavigationRoute(AppRoutes.dashboard, NavigationRouter()), - arguments: success.session); + arguments: session); } else { - _goToLogin(); + logError('SessionController::_goToMailboxDashBoard(): apiUrl is NULL'); + performInvokeLogoutAction(); } } - - @override - void onDone() {} } \ No newline at end of file diff --git a/lib/features/session/presentation/session_page_bindings.dart b/lib/features/session/presentation/session_page_bindings.dart index a9b791d57b..b81105d488 100644 --- a/lib/features/session/presentation/session_page_bindings.dart +++ b/lib/features/session/presentation/session_page_bindings.dart @@ -2,103 +2,43 @@ import 'package:core/data/network/config/dynamic_url_interceptors.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/caching/caching_manager.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/account_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/datasource_impl/hive_account_datasource_impl.dart'; -import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; -import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; -import 'package:tmail_ui_user/features/login/data/network/config/authorization_interceptors.dart'; -import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; -import 'package:tmail_ui_user/features/login/data/repository/account_repository_impl.dart'; -import 'package:tmail_ui_user/features/login/data/repository/authentication_oidc_repository_impl.dart'; import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/get_credential_interactor.dart'; -import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_token_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/update_authentication_account_interactor.dart'; -import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/session/domain/repository/session_repository.dart'; import 'package:tmail_ui_user/features/session/domain/usecases/get_session_interactor.dart'; +import 'package:tmail_ui_user/features/session/domain/usecases/get_stored_session_interactor.dart'; import 'package:tmail_ui_user/features/session/presentation/session_controller.dart'; -import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; -import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; class SessionPageBindings extends BaseBindings { @override void bindingsController() { Get.lazyPut(() => SessionController( - Get.find(), - Get.find(), Get.find(), Get.find(), Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), Get.find(), Get.find(), + Get.find(), )); } @override - void bindingsDataSource() { - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); - } + void bindingsDataSource() {} @override - void bindingsDataSourceImpl() { - Get.lazyPut(() => AuthenticationOIDCDataSourceImpl( - Get.find(), - Get.find(), - Get.find(), - Get.find(), - Get.find(), - )); - Get.lazyPut(() => HiveAccountDatasourceImpl( - Get.find(), - Get.find())); - } + void bindingsDataSourceImpl() {} @override void bindingsInteractor() { - Get.lazyPut(() => DeleteAuthorityOidcInteractor( - Get.find(), - Get.find())); - Get.lazyPut(() => LogoutOidcInteractor( - Get.find(), - Get.find(), - )); - Get.lazyPut(() => GetStoredTokenOidcInteractor( - Get.find(), - Get.find(), - )); - Get.lazyPut(() => GetAuthenticatedAccountInteractor( - Get.find(), - Get.find(), - Get.find(), - )); Get.lazyPut(() => UpdateAuthenticationAccountInteractor(Get.find())); + Get.lazyPut(() => GetStoredSessionInteractor(Get.find())); } @override - void bindingsRepository() { - Get.lazyPut(() => Get.find()); - Get.lazyPut(() => Get.find()); - } + void bindingsRepository() {} @override - void bindingsRepositoryImpl() { - Get.lazyPut(() => AuthenticationOIDCRepositoryImpl(Get.find())); - Get.lazyPut(() => AccountRepositoryImpl(Get.find())); - } + void bindingsRepositoryImpl() {} } \ No newline at end of file diff --git a/lib/features/thread/data/datasource/thread_datasource.dart b/lib/features/thread/data/datasource/thread_datasource.dart index fbb4fa334e..ac6c1b02c5 100644 --- a/lib/features/thread/data/datasource/thread_datasource.dart +++ b/lib/features/thread/data/datasource/thread_datasource.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/presentation_email.dart'; @@ -15,6 +17,7 @@ import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option abstract class ThreadDataSource { Future getAllEmail( + Session session, AccountId accountId, { UnsignedInt? limit, @@ -25,6 +28,7 @@ abstract class ThreadDataSource { ); Future getChanges( + Session session, AccountId accountId, State sinceState, { @@ -33,15 +37,25 @@ abstract class ThreadDataSource { } ); - Future> getAllEmailCache({MailboxId? inMailboxId, Set? sort, FilterMessageOption? filterOption, UnsignedInt? limit}); + Future> getAllEmailCache( + AccountId accountId, + UserName userName, + { + MailboxId? inMailboxId, + Set? sort, + FilterMessageOption? filterOption, + UnsignedInt? limit + } + ); - Future update({List? updated, List? created, List? destroyed}); + Future update(AccountId accountId, UserName userName, {List? updated, List? created, List? destroyed}); - Future> emptyTrashFolder( - AccountId accountId, - MailboxId mailboxId, - Future Function(List? newDestroyed) updateDestroyedEmailCache, + Future> emptyMailboxFolder( + Session session, + AccountId accountId, + MailboxId mailboxId, + Future Function(List? newDestroyed) updateDestroyedEmailCache, ); - Future getEmailById(AccountId accountId, EmailId emailId, {Properties? properties}); + Future getEmailById(Session session, AccountId accountId, EmailId emailId, {Properties? properties}); } \ No newline at end of file diff --git a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart index 5130d03359..6047005d00 100644 --- a/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/local_thread_datasource_impl.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/presentation_email.dart'; @@ -25,31 +27,35 @@ class LocalThreadDataSourceImpl extends ThreadDataSource { @override Future getAllEmail( - AccountId accountId, - { - UnsignedInt? limit, - Set? sort, - Filter? filter, - Properties? properties - } + Session session, + AccountId accountId, + { + UnsignedInt? limit, + Set? sort, + Filter? filter, + Properties? properties + } ) { throw UnimplementedError(); } @override Future getChanges( - AccountId accountId, - State sinceState, - { - Properties? propertiesCreated, - Properties? propertiesUpdated - } + Session session, + AccountId accountId, + State sinceState, + { + Properties? propertiesCreated, + Properties? propertiesUpdated + } ) { throw UnimplementedError(); } @override - Future> getAllEmailCache({ + Future> getAllEmailCache( + AccountId accountId, + UserName userName, { MailboxId? inMailboxId, Set? sort, FilterMessageOption? filterOption, @@ -57,33 +63,36 @@ class LocalThreadDataSourceImpl extends ThreadDataSource { }) { return Future.sync(() async { return await _emailCacheManager.getAllEmail( + accountId, + userName, inMailboxId: inMailboxId, sort: sort, filterOption: filterOption ?? FilterMessageOption.all, limit: limit); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future update({ + Future update( + AccountId accountId, + UserName userName, { List? updated, List? created, List? destroyed }) { return Future.sync(() async { return await _emailCacheManager.update( + accountId, + userName, updated: updated, created: created, destroyed: destroyed); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future> emptyTrashFolder( + Future> emptyMailboxFolder( + Session session, AccountId accountId, MailboxId mailboxId, Future Function(List? newDestroyed) updateDestroyedEmailCache @@ -92,7 +101,7 @@ class LocalThreadDataSourceImpl extends ThreadDataSource { } @override - Future getEmailById(AccountId accountId, EmailId emailId, {Properties? properties}) { + Future getEmailById(Session session, AccountId accountId, EmailId emailId, {Properties? properties}) { throw UnimplementedError(); } } \ No newline at end of file diff --git a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart index 71aa53470a..1175f8dc0b 100644 --- a/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart +++ b/lib/features/thread/data/datasource_impl/thread_datasource_impl.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/presentation_email.dart'; @@ -32,6 +34,7 @@ class ThreadDataSourceImpl extends ThreadDataSource { @override Future getAllEmail( + Session session, AccountId accountId, { UnsignedInt? limit, @@ -42,66 +45,67 @@ class ThreadDataSourceImpl extends ThreadDataSource { ) { return Future.sync(() async { return await threadAPI.getAllEmail( + session, accountId, limit: limit, sort: sort, filter: filter, properties: properties); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override Future getChanges( - AccountId accountId, - State sinceState, - { - Properties? propertiesCreated, - Properties? propertiesUpdated - } + Session session, + AccountId accountId, + State sinceState, + { + Properties? propertiesCreated, + Properties? propertiesUpdated + } ) { return Future.sync(() async { return await threadAPI.getChanges( + session, accountId, sinceState, propertiesCreated: propertiesCreated, propertiesUpdated: propertiesUpdated); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future> getAllEmailCache({MailboxId? inMailboxId, Set? sort, FilterMessageOption? filterOption, UnsignedInt? limit}) { + Future> getAllEmailCache(AccountId accountId, UserName userName, {MailboxId? inMailboxId, Set? sort, FilterMessageOption? filterOption, UnsignedInt? limit}) { throw UnimplementedError(); } @override - Future update({List? updated, List? created, List? destroyed}) { + Future update(AccountId accountId, UserName userName, {List? updated, List? created, List? destroyed}) { throw UnimplementedError(); } @override - Future> emptyTrashFolder(AccountId accountId, MailboxId mailboxId, Future Function(List? newDestroyed) updateDestroyedEmailCache) { + Future> emptyMailboxFolder( + Session session, + AccountId accountId, + MailboxId mailboxId, + Future Function(List? newDestroyed) updateDestroyedEmailCache + ) { return Future.sync(() async { - return await _threadIsolateWorker.emptyTrashFolder( - accountId, - mailboxId, - updateDestroyedEmailCache, + return await _threadIsolateWorker.emptyMailboxFolder( + session, + accountId, + mailboxId, + updateDestroyedEmailCache, ); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } @override - Future getEmailById(AccountId accountId, EmailId emailId, {Properties? properties}) { + Future getEmailById(Session session, AccountId accountId, EmailId emailId, {Properties? properties}) { return Future.sync(() async { - final email = await threadAPI.getEmailById(accountId, emailId, properties: properties); + final email = await threadAPI.getEmailById(session, accountId, emailId, properties: properties); return email.toPresentationEmail(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/thread/data/extensions/email_cache_extension.dart b/lib/features/thread/data/extensions/email_cache_extension.dart index 83ba6ce7a2..5d101f04ab 100644 --- a/lib/features/thread/data/extensions/email_cache_extension.dart +++ b/lib/features/thread/data/extensions/email_cache_extension.dart @@ -4,6 +4,7 @@ import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/cleanup/domain/model/email_cleanup_rule.dart'; @@ -13,7 +14,7 @@ import 'package:tmail_ui_user/features/thread/data/extensions/email_address_hive extension EmailCacheExtension on EmailCache { Email toEmail() { return Email( - EmailId(Id(id)), + id: EmailId(Id(id)), keywords: keywords != null ? Map.fromIterables(keywords!.keys.map((value) => KeyWordIdentifier(value)), keywords!.values) : null, @@ -31,6 +32,9 @@ extension EmailCacheExtension on EmailCache { mailboxIds: mailboxIds != null ? Map.fromIterables(mailboxIds!.keys.map((value) => MailboxId(Id(value))), mailboxIds!.values) : null, + headerCalendarEvent: headerCalendarEvent != null + ? Map.fromIterables(headerCalendarEvent!.keys.map((value) => IndividualHeaderIdentifier(value)), headerCalendarEvent!.values) + : null ); } diff --git a/lib/features/thread/data/extensions/email_extension.dart b/lib/features/thread/data/extensions/email_extension.dart index f5a295399a..d6a02dd60d 100644 --- a/lib/features/thread/data/extensions/email_extension.dart +++ b/lib/features/thread/data/extensions/email_extension.dart @@ -1,4 +1,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/map_header_identifier_id_extension.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; import 'package:tmail_ui_user/features/thread/data/extensions/map_keywords_extension.dart'; import 'package:tmail_ui_user/features/thread/data/extensions/email_address_extension.dart'; @@ -8,7 +10,7 @@ extension EmailExtension on Email { EmailCache toEmailCache() { return EmailCache( - id.id.value, + id!.id.value, keywords: keywords?.toMapString(), size: size?.value.round(), receivedAt: receivedAt?.value, @@ -22,6 +24,13 @@ extension EmailExtension on Email { bcc: bcc?.map((emailAddress) => emailAddress.toEmailAddressHiveCache()).toList(), replyTo: replyTo?.map((emailAddress) => emailAddress.toEmailAddressHiveCache()).toList(), mailboxIds: mailboxIds?.toMapString(), + headerCalendarEvent: headerCalendarEvent?.toMapString(), ); } + + bool belongTo(MailboxId mailboxId) { + return mailboxIds != null + && mailboxIds!.containsKey(mailboxId) + && mailboxIds![mailboxId] == true; + } } \ No newline at end of file diff --git a/lib/features/thread/data/extensions/list_email_cache_extension.dart b/lib/features/thread/data/extensions/list_email_cache_extension.dart index cb233cc27a..7753354755 100644 --- a/lib/features/thread/data/extensions/list_email_cache_extension.dart +++ b/lib/features/thread/data/extensions/list_email_cache_extension.dart @@ -1,8 +1,8 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/email_cache_extension.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; extension ListEmailCacheExtension on List { - Map toMap() { - return { for (var emailCache in this) emailCache.id : emailCache }; - } + List toEmailList() => map((emailCache) => emailCache.toEmail()).toList(); } \ No newline at end of file diff --git a/lib/features/thread/data/extensions/list_email_extension.dart b/lib/features/thread/data/extensions/list_email_extension.dart new file mode 100644 index 0000000000..9640dffee6 --- /dev/null +++ b/lib/features/thread/data/extensions/list_email_extension.dart @@ -0,0 +1,18 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:model/extensions/email_id_extensions.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/email_extension.dart'; +import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; + +extension ListEmailExtension on List { + Map toMapCache(AccountId accountId, UserName userName) { + return { + for (var email in this) + TupleKey(email.id!.asString, accountId.asString, userName.value).encodeKey : email.toEmailCache() + }; + } +} \ No newline at end of file diff --git a/lib/features/thread/data/extensions/list_email_id_extension.dart b/lib/features/thread/data/extensions/list_email_id_extension.dart new file mode 100644 index 0000000000..f164824263 --- /dev/null +++ b/lib/features/thread/data/extensions/list_email_id_extension.dart @@ -0,0 +1,12 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/extensions/account_id_extensions.dart'; +import 'package:model/extensions/email_id_extensions.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; + +extension ListEmailIdExtension on List { + List toCacheKeyList(AccountId accountId, UserName userName) => + map((id) => TupleKey(id.asString, accountId.asString, userName.value).encodeKey).toList(); +} \ No newline at end of file diff --git a/lib/features/thread/data/extensions/map_header_identifier_id_extension.dart b/lib/features/thread/data/extensions/map_header_identifier_id_extension.dart new file mode 100644 index 0000000000..081749cc40 --- /dev/null +++ b/lib/features/thread/data/extensions/map_header_identifier_id_extension.dart @@ -0,0 +1,7 @@ + +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; + +extension MapHeaderIdentifierExtension on Map { + + Map toMapString() => Map.fromIterables(keys.map((identifier) => identifier.value), values); +} \ No newline at end of file diff --git a/lib/features/thread/data/local/email_cache_manager.dart b/lib/features/thread/data/local/email_cache_manager.dart index 0fb9a004b8..0e37fe9d51 100644 --- a/lib/features/thread/data/local/email_cache_manager.dart +++ b/lib/features/thread/data/local/email_cache_manager.dart @@ -1,15 +1,20 @@ -import 'package:core/core.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/model.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:tmail_ui_user/features/caching/email_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/email_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/utils/cache_utils.dart'; import 'package:tmail_ui_user/features/cleanup/domain/model/email_cleanup_rule.dart'; +import 'package:tmail_ui_user/features/email/domain/exceptions/email_cache_exceptions.dart'; import 'package:tmail_ui_user/features/thread/data/extensions/email_cache_extension.dart'; import 'package:tmail_ui_user/features/thread/data/extensions/email_extension.dart'; import 'package:tmail_ui_user/features/thread/data/extensions/list_email_cache_extension.dart'; -import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/list_email_extension.dart'; +import 'package:tmail_ui_user/features/thread/data/extensions/list_email_id_extension.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; +import 'package:tmail_ui_user/features/thread/data/model/email_cache.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; class EmailCacheManager { @@ -18,19 +23,20 @@ class EmailCacheManager { EmailCacheManager(this._emailCacheClient); - Future> getAllEmail({ + Future> getAllEmail( + AccountId accountId, + UserName userName, { MailboxId? inMailboxId, Set? sort, UnsignedInt? limit, FilterMessageOption filterOption = FilterMessageOption.all }) async { - final emailCacheList = inMailboxId != null - ? await _emailCacheClient.getListEmailCacheByMailboxId(inMailboxId) - : await _emailCacheClient.getAll(); + final emailCacheList = await _emailCacheClient.getListByTupleKey(accountId.asString, userName.value); final emailList = emailCacheList - .map((emailCache) => emailCache.toEmail()) - .where((email) => filterOption.filterEmail(email)) - .toList(); + .toEmailList() + .where((email) => _filterEmailByMailbox(email, filterOption, inMailboxId)) + .toList(); + if (sort != null) { for (var comparator in sort) { emailList.sortBy(comparator); @@ -43,28 +49,35 @@ class EmailCacheManager { return emailList; } - Future update({List? updated, List? created, List? destroyed}) async { + bool _filterEmailByMailbox(Email email, FilterMessageOption option, MailboxId? inMailboxId) { + if (inMailboxId != null) { + return email.belongTo(inMailboxId) && option.filterEmail(email); + } else { + return option.filterEmail(email); + } + } + + Future update( + AccountId accountId, + UserName userName, { + List? updated, + List? created, + List? destroyed + }) async { final emailCacheExist = await _emailCacheClient.isExistTable(); - log('EmailCacheManager::update(): emailCacheExist: $emailCacheExist'); if (emailCacheExist) { - final updatedCacheEmails = updated - ?.map((email) => email.toEmailCache()).toList() ?? []; - final createdCacheEmails = created - ?.map((email) => email.toEmailCache()).toList() ?? []; - final destroyedCacheEmails = destroyed - ?.map((emailId) => emailId.id.value).toList() ?? []; - - log('EmailCacheManager::update(): destroyedCacheEmails: ${destroyedCacheEmails.length}'); + final updatedCacheEmails = updated?.toMapCache(accountId, userName) ?? {}; + final createdCacheEmails = created?.toMapCache(accountId, userName) ?? {}; + final destroyedCacheEmails = destroyed?.toCacheKeyList(accountId, userName) ?? []; await Future.wait([ - _emailCacheClient.updateMultipleItem(updatedCacheEmails.toMap()), - _emailCacheClient.insertMultipleItem(createdCacheEmails.toMap()), + _emailCacheClient.updateMultipleItem(updatedCacheEmails), + _emailCacheClient.insertMultipleItem(createdCacheEmails), _emailCacheClient.deleteMultipleItem(destroyedCacheEmails) ]); } else { - final createdCacheEmails = created - ?.map((email) => email.toEmailCache()).toList() ?? []; - await _emailCacheClient.insertMultipleItem(createdCacheEmails.toMap()); + final createdCacheEmails = created?.toMapCache(accountId, userName) ?? {}; + await _emailCacheClient.insertMultipleItem(createdCacheEmails); } } @@ -79,4 +92,19 @@ class EmailCacheManager { await _emailCacheClient.deleteMultipleItem(listEmailIdCacheExpire); } } + + Future storeEmail(AccountId accountId, UserName userName, EmailCache emailCache) { + final keyCache = TupleKey(emailCache.id, accountId.asString, userName.value).encodeKey; + return _emailCacheClient.insertItem(keyCache, emailCache); + } + + Future getStoredEmail(AccountId accountId, UserName userName, EmailId emailId) async { + final keyCache = TupleKey(emailId.asString, accountId.asString, userName.value).encodeKey; + final emailCache = await _emailCacheClient.getItem(keyCache, needToReopen: true); + if (emailCache != null) { + return emailCache; + } else { + throw NotFoundStoredEmailException(); + } + } } \ No newline at end of file diff --git a/lib/features/thread/data/model/email_cache.dart b/lib/features/thread/data/model/email_cache.dart index a407d5125c..6db80e1d50 100644 --- a/lib/features/thread/data/model/email_cache.dart +++ b/lib/features/thread/data/model/email_cache.dart @@ -51,6 +51,9 @@ class EmailCache extends HiveObject with EquatableMixin { @HiveField(13) Map? mailboxIds; + @HiveField(14) + Map? headerCalendarEvent; + EmailCache( this.id, { @@ -67,6 +70,7 @@ class EmailCache extends HiveObject with EquatableMixin { this.bcc, this.replyTo, this.mailboxIds, + this.headerCalendarEvent, } ); @@ -86,5 +90,6 @@ class EmailCache extends HiveObject with EquatableMixin { preview, hasAttachment, mailboxIds, + headerCalendarEvent, ]; } \ No newline at end of file diff --git a/lib/features/thread/data/model/empty_trash_folder_arguments.dart b/lib/features/thread/data/model/empty_mailbox_folder_arguments.dart similarity index 57% rename from lib/features/thread/data/model/empty_trash_folder_arguments.dart rename to lib/features/thread/data/model/empty_mailbox_folder_arguments.dart index fc3e893993..d45d8f24ec 100644 --- a/lib/features/thread/data/model/empty_trash_folder_arguments.dart +++ b/lib/features/thread/data/model/empty_mailbox_folder_arguments.dart @@ -1,22 +1,31 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; -class EmptyTrashFolderArguments with EquatableMixin { +class EmptyMailboxFolderArguments with EquatableMixin { + final Session session; final AccountId accountId; - final MailboxId trashMailboxId; + final MailboxId mailboxId; final ThreadAPI threadAPI; final EmailAPI emailAPI; - EmptyTrashFolderArguments( + EmptyMailboxFolderArguments( + this.session, this.threadAPI, this.emailAPI, this.accountId, - this.trashMailboxId, + this.mailboxId, ); @override - List get props => [accountId, trashMailboxId]; + List get props => [ + session, + accountId, + emailAPI, + threadAPI, + mailboxId + ]; } diff --git a/lib/features/thread/data/network/thread_api.dart b/lib/features/thread/data/network/thread_api.dart index a00084af42..a8737ae4d6 100644 --- a/lib/features/thread/data/network/thread_api.dart +++ b/lib/features/thread/data/network/thread_api.dart @@ -5,6 +5,7 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/request/reference_path.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; @@ -17,6 +18,7 @@ import 'package:jmap_dart_client/jmap/mail/email/get/get_email_response.dart'; import 'package:jmap_dart_client/jmap/mail/email/query/query_email_method.dart'; import 'package:tmail_ui_user/features/thread/data/model/email_change_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; +import 'package:tmail_ui_user/main/error/capability_validator.dart'; class ThreadAPI { @@ -25,6 +27,7 @@ class ThreadAPI { ThreadAPI(this.httpClient); Future getAllEmail( + Session session, AccountId accountId, { UnsignedInt? limit, @@ -57,8 +60,11 @@ class ThreadAPI { final getEmailInvocation = jmapRequestBuilder.invocation(getEmailMethod); + final capabilities = getEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final result = await (jmapRequestBuilder - ..usings(getEmailMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -75,6 +81,7 @@ class ThreadAPI { } Future getChanges( + Session session, AccountId accountId, State sinceState, { @@ -111,8 +118,11 @@ class ThreadAPI { final getEmailUpdatedInvocation = jmapRequestBuilder.invocation(getMailboxUpdated); final getEmailCreatedInvocation = jmapRequestBuilder.invocation(getEmailCreated); + final capabilities = getEmailCreated.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final result = await (jmapRequestBuilder - ..usings(getEmailCreated.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); @@ -140,7 +150,7 @@ class ThreadAPI { updatedProperties: propertiesUpdated); } - Future getEmailById(AccountId accountId, EmailId emailId, {Properties? properties}) async { + Future getEmailById(Session session, AccountId accountId, EmailId emailId, {Properties? properties}) async { final processingInvocation = ProcessingInvocation(); final jmapRequestBuilder = JmapRequestBuilder(httpClient, processingInvocation); @@ -153,8 +163,11 @@ class ThreadAPI { final getEmailInvocation = jmapRequestBuilder.invocation(getEmailMethod); + final capabilities = getEmailMethod.requiredCapabilities + .toCapabilitiesSupportTeamMailboxes(session, accountId); + final result = await (jmapRequestBuilder - ..usings(getEmailMethod.requiredCapabilities)) + ..usings(capabilities)) .build() .execute(); diff --git a/lib/features/thread/data/network/thread_isolate_worker.dart b/lib/features/thread/data/network/thread_isolate_worker.dart index 445641df51..89c75ea493 100644 --- a/lib/features/thread/data/network/thread_isolate_worker.dart +++ b/lib/features/thread/data/network/thread_isolate_worker.dart @@ -1,9 +1,10 @@ import 'dart:async'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator.dart'; @@ -11,9 +12,11 @@ import 'package:jmap_dart_client/jmap/mail/email/email_comparator_property.dart' import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/email_property.dart'; +import 'package:model/extensions/list_email_extension.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; -import 'package:tmail_ui_user/features/thread/data/model/empty_trash_folder_arguments.dart'; +import 'package:tmail_ui_user/features/thread/data/model/empty_mailbox_folder_arguments.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; +import 'package:tmail_ui_user/features/thread/domain/exceptions/thread_exceptions.dart'; import 'package:worker_manager/worker_manager.dart'; class ThreadIsolateWorker { @@ -23,57 +26,63 @@ class ThreadIsolateWorker { ThreadIsolateWorker(this._threadAPI, this._emailAPI, this._isolateExecutor); - Future> emptyTrashFolder( + Future> emptyMailboxFolder( + Session session, AccountId accountId, MailboxId mailboxId, Future Function(List? newDestroyed) updateDestroyedEmailCache, ) async { - if (BuildUtils.isWeb) { - return _emptyTrashFolderOnWeb(accountId, mailboxId, updateDestroyedEmailCache); + if (PlatformInfo.isWeb) { + return _emptyMailboxFolderOnWeb(session, accountId, mailboxId, updateDestroyedEmailCache); } else { final result = await _isolateExecutor.execute( - arg1: EmptyTrashFolderArguments(_threadAPI, _emailAPI, accountId, mailboxId), - fun1: _emptyTrashFolderAction, - notification: (value) { - if (value is List) { - updateDestroyedEmailCache.call(value); - log('ThreadIsolateWorker::emptyTrashFolder(): onUpdateProgress: PERCENT ${value.length}'); - } - }); - return result; + arg1: EmptyMailboxFolderArguments(session, _threadAPI, _emailAPI, accountId, mailboxId), + fun1: _emptyMailboxFolderAction, + notification: (value) { + if (value is List) { + updateDestroyedEmailCache.call(value); + log('ThreadIsolateWorker::emptyMailboxFolder(): onUpdateProgress: PERCENT ${value.length}'); + } + } + ); + + if (result.isEmpty) { + throw NotFoundEmailsDeletedException(); + } else { + return result; + } } } - static Future> _emptyTrashFolderAction(EmptyTrashFolderArguments args, TypeSendPort sendPort) async { + static Future> _emptyMailboxFolderAction(EmptyMailboxFolderArguments args, TypeSendPort sendPort) async { List emailListCompleted = List.empty(growable: true); try { var hasEmails = true; + Email? lastEmail; while (hasEmails) { - Email? lastEmail; - - final emailsResponse = await args.threadAPI.getAllEmail(args.accountId, - sort: {}..add( - EmailComparator(EmailComparatorProperty.receivedAt) - ..setIsAscending(false)), - filter: EmailFilterCondition(inMailbox: args.trashMailboxId, before: lastEmail?.receivedAt), - properties: Properties({EmailProperty.id})); + final emailsResponse = await args.threadAPI.getAllEmail( + args.session, + args.accountId, + sort: {}..add( + EmailComparator(EmailComparatorProperty.receivedAt) + ..setIsAscending(false)), + filter: EmailFilterCondition(inMailbox: args.mailboxId, before: lastEmail?.receivedAt), + properties: Properties({EmailProperty.id})); var newEmailList = emailsResponse.emailList ?? []; if (lastEmail != null) { newEmailList = newEmailList.where((email) => email.id != lastEmail!.id).toList(); } - log('ThreadIsolateWorker::_emptyTrashFolderAction(): ${newEmailList.length}'); + log('ThreadIsolateWorker::_emptyMailboxFolderAction(): ${newEmailList.length}'); - if (newEmailList.isNotEmpty == true) { + if (newEmailList.isNotEmpty) { lastEmail = newEmailList.last; hasEmails = true; - final emailIds = newEmailList.map((email) => email.id).toList(); + final listEmailIdDeleted = await args.emailAPI.deleteMultipleEmailsPermanently(args.session, args.accountId, newEmailList.listEmailIds); - final listEmailIdDeleted = await args.emailAPI.deleteMultipleEmailsPermanently(args.accountId, emailIds); - - if (listEmailIdDeleted.isNotEmpty && listEmailIdDeleted.length == emailIds.length) { + if (listEmailIdDeleted.isNotEmpty && listEmailIdDeleted.length == newEmailList.listEmailIds.length) { sendPort.send(listEmailIdDeleted); } emailListCompleted.addAll(listEmailIdDeleted); @@ -84,46 +93,46 @@ class ThreadIsolateWorker { } } } catch (e) { - log('ThreadIsolateWorker::_emptyTrashFolderAction(): ERROR: $e'); + log('ThreadIsolateWorker::_emptyMailboxFolderAction(): ERROR: $e'); } - log('ThreadIsolateWorker::_emptyTrashFolderAction(): TOTAL_REMOVE: ${emailListCompleted.length}'); + log('ThreadIsolateWorker::_emptyMailboxFolderAction(): TOTAL_REMOVE: ${emailListCompleted.length}'); return emailListCompleted; } - Future> _emptyTrashFolderOnWeb( + Future> _emptyMailboxFolderOnWeb( + Session session, AccountId accountId, - MailboxId trashMailboxId, + MailboxId mailboxId, Future Function(List newDestroyed) updateDestroyedEmailCache, ) async { List emailListCompleted = List.empty(growable: true); try { var hasEmails = true; + Email? lastEmail; while (hasEmails) { - Email? lastEmail; - - final emailsResponse = await _threadAPI.getAllEmail(accountId, - sort: {}..add( - EmailComparator(EmailComparatorProperty.receivedAt) - ..setIsAscending(false)), - filter: EmailFilterCondition(inMailbox: trashMailboxId, before: lastEmail?.receivedAt), - properties: Properties({EmailProperty.id})); + final emailsResponse = await _threadAPI.getAllEmail( + session, + accountId, + sort: {}..add( + EmailComparator(EmailComparatorProperty.receivedAt) + ..setIsAscending(false)), + filter: EmailFilterCondition(inMailbox: mailboxId, before: lastEmail?.receivedAt), + properties: Properties({EmailProperty.id})); var newEmailList = emailsResponse.emailList ?? []; if (lastEmail != null) { newEmailList = newEmailList.where((email) => email.id != lastEmail!.id).toList(); } - log('ThreadIsolateWorker::_emptyTrashFolderOnWeb(): ${newEmailList.length}'); + log('ThreadIsolateWorker::_emptyMailboxFolderOnWeb(): ${newEmailList.length}'); - if (newEmailList.isNotEmpty == true) { + if (newEmailList.isNotEmpty) { lastEmail = newEmailList.last; hasEmails = true; - final emailIds = newEmailList.map((email) => email.id).toList(); - - final listEmailIdDeleted = await _emailAPI.deleteMultipleEmailsPermanently(accountId, emailIds); + final listEmailIdDeleted = await _emailAPI.deleteMultipleEmailsPermanently(session, accountId, newEmailList.listEmailIds); - if (listEmailIdDeleted.isNotEmpty && listEmailIdDeleted.length == emailIds.length) { + if (listEmailIdDeleted.isNotEmpty && listEmailIdDeleted.length == newEmailList.listEmailIds.length) { await updateDestroyedEmailCache(listEmailIdDeleted); } emailListCompleted.addAll(listEmailIdDeleted); @@ -133,9 +142,9 @@ class ThreadIsolateWorker { } } } catch (e) { - log('ThreadIsolateWorker::_emptyTrashFolderOnWeb(): ERROR: $e'); + log('ThreadIsolateWorker::_emptyMailboxFolderOnWeb(): ERROR: $e'); } - log('ThreadIsolateWorker::_emptyTrashFolderOnWeb(): TOTAL_REMOVE: ${emailListCompleted.length}'); + log('ThreadIsolateWorker::_emptyMailboxFolderOnWeb(): TOTAL_REMOVE: ${emailListCompleted.length}'); return emailListCompleted; } } diff --git a/lib/features/thread/data/repository/thread_repository_impl.dart b/lib/features/thread/data/repository/thread_repository_impl.dart index 7c3404142a..acd9a2a8a7 100644 --- a/lib/features/thread/data/repository/thread_repository_impl.dart +++ b/lib/features/thread/data/repository/thread_repository_impl.dart @@ -1,12 +1,15 @@ -import 'package:core/core.dart'; +import 'package:core/data/model/source_type/data_source_type.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart' as dartz; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_filter_condition.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; @@ -32,6 +35,7 @@ class ThreadRepositoryImpl extends ThreadRepository { @override Stream getAllEmail( + Session session, AccountId accountId, { UnsignedInt? limit, @@ -44,11 +48,13 @@ class ThreadRepositoryImpl extends ThreadRepository { log('ThreadRepositoryImpl::getAllEmail(): filter = ${emailFilter?.mailboxId}'); final localEmailResponse = await Future.wait([ mapDataSource[DataSourceType.local]!.getAllEmailCache( - inMailboxId: emailFilter?.mailboxId, - sort: sort, - limit: limit, - filterOption: emailFilter?.filterOption), - stateDataSource.getState(StateType.email) + accountId, + session.username, + inMailboxId: emailFilter?.mailboxId, + sort: sort, + limit: limit, + filterOption: emailFilter?.filterOption), + stateDataSource.getState(accountId, session.username, StateType.email) ]).then((List response) { return EmailsResponse(emailList: response.first, state: response.last); }); @@ -58,6 +64,7 @@ class ThreadRepositoryImpl extends ThreadRepository { if (!localEmailResponse.hasEmails() || (localEmailResponse.emailList?.length ?? 0) < ThreadConstants.defaultLimit.value) { networkEmailResponse = await mapDataSource[DataSourceType.network]!.getAllEmail( + session, accountId, limit: limit, sort: sort, @@ -65,6 +72,7 @@ class ThreadRepositoryImpl extends ThreadRepository { properties: propertiesCreated); if (_isApproveFilterOption(emailFilter?.filterOption, networkEmailResponse.emailList)) { _getFirstPage( + session, accountId, sort: sort, mailboxId: emailFilter?.mailboxId, @@ -77,12 +85,13 @@ class ThreadRepositoryImpl extends ThreadRepository { } if (networkEmailResponse != null) { - await _updateEmailCache(newCreated: networkEmailResponse.emailList); + await _updateEmailCache(accountId, session.username, newCreated: networkEmailResponse.emailList); } if (localEmailResponse.hasState()) { log('ThreadRepositoryImpl::getAllEmail(): filter = ${emailFilter?.mailboxId} local has state: ${localEmailResponse.state}'); await _synchronizeCacheWithChanges( + session, accountId, localEmailResponse.state!, propertiesCreated: propertiesCreated, @@ -92,18 +101,20 @@ class ThreadRepositoryImpl extends ThreadRepository { if (networkEmailResponse != null) { log('ThreadRepositoryImpl::getAllEmail(): filter = ${emailFilter?.mailboxId} no local state -> update from network: ${networkEmailResponse.state}'); if (networkEmailResponse.state != null) { - await _updateState(networkEmailResponse.state!); + await _updateState(accountId, session.username, networkEmailResponse.state!); } } } final newEmailResponse = await Future.wait([ mapDataSource[DataSourceType.local]!.getAllEmailCache( - inMailboxId: emailFilter?.mailboxId, - sort: sort, - limit: limit, - filterOption: emailFilter?.filterOption), - stateDataSource.getState(StateType.email) + accountId, + session.username, + inMailboxId: emailFilter?.mailboxId, + sort: sort, + limit: limit, + filterOption: emailFilter?.filterOption), + stateDataSource.getState(accountId, session.username, StateType.email) ]).then((List response) { return EmailsResponse(emailList: response.first, state: response.last); }); @@ -116,6 +127,7 @@ class ThreadRepositoryImpl extends ThreadRepository { } Future _getFirstPage( + Session session, AccountId accountId, { Set? sort, @@ -125,13 +137,14 @@ class ThreadRepositoryImpl extends ThreadRepository { } ) async { final networkEmailResponse = await mapDataSource[DataSourceType.network]!.getAllEmail( + session, accountId, limit: ThreadConstants.defaultLimit, sort: sort, filter: filter ?? EmailFilterCondition(inMailbox: mailboxId), properties: propertiesCreated, ); - await _updateEmailCache(newCreated: networkEmailResponse.emailList); + await _updateEmailCache(accountId, session.username, newCreated: networkEmailResponse.emailList); return networkEmailResponse; } @@ -157,7 +170,9 @@ class ThreadRepositoryImpl extends ThreadRepository { } dartz.Tuple2 _combineUpdatedWithEmailInCache(Email updatedEmail, List? emailCacheList) { - final emailOld = emailCacheList?.findEmailById(updatedEmail.id); + final emailOld = updatedEmail.id != null + ? emailCacheList?.findEmailById(updatedEmail.id!) + : null; if (emailOld != null) { log('ThreadRepositoryImpl::_combineUpdatedWithEmailInCache(): cache hit'); return dartz.Tuple2(updatedEmail, emailOld); @@ -167,35 +182,41 @@ class ThreadRepositoryImpl extends ThreadRepository { } } - Future _updateEmailCache({ + Future _updateEmailCache( + AccountId accountId, + UserName userName, { List? newUpdated, List? newCreated, List? newDestroyed }) async { await mapDataSource[DataSourceType.local]!.update( + accountId, + userName, updated: newUpdated, created: newCreated, destroyed: newDestroyed); } - Future _updateState(State newState) async { + Future _updateState(AccountId accountId, UserName userName, State newState) async { log('ThreadRepositoryImpl::_updateState(): [MAIL] $newState'); - await stateDataSource.saveState(newState.toStateCache(StateType.email)); + await stateDataSource.saveState(accountId, userName, newState.toStateCache(StateType.email)); } @override Stream refreshChanges( - AccountId accountId, - State currentState, - { - Set? sort, - EmailFilter? emailFilter, - Properties? propertiesCreated, - Properties? propertiesUpdated, - } + Session session, + AccountId accountId, + State currentState, + { + Set? sort, + EmailFilter? emailFilter, + Properties? propertiesCreated, + Properties? propertiesUpdated, + } ) async* { log('ThreadRepositoryImpl::refreshChanges(): $currentState'); await _synchronizeCacheWithChanges( + session, accountId, currentState, propertiesCreated: propertiesCreated, @@ -203,8 +224,14 @@ class ThreadRepositoryImpl extends ThreadRepository { ); final newEmailResponse = await Future.wait([ - mapDataSource[DataSourceType.local]!.getAllEmailCache(inMailboxId: emailFilter?.mailboxId, sort: sort, filterOption: emailFilter?.filterOption), - stateDataSource.getState(StateType.email) + mapDataSource[DataSourceType.local]!.getAllEmailCache( + accountId, + session.username, + inMailboxId: emailFilter?.mailboxId, + sort: sort, + filterOption: emailFilter?.filterOption + ), + stateDataSource.getState(accountId, session.username, StateType.email) ]).then((List response) { return EmailsResponse(emailList: response.first, state: response.last); }); @@ -212,6 +239,7 @@ class ThreadRepositoryImpl extends ThreadRepository { if (!newEmailResponse.hasEmails() || (newEmailResponse.emailList?.length ?? 0) < ThreadConstants.defaultLimit.value) { final networkEmailResponse = await _getFirstPage( + session, accountId, sort: sort, filter: emailFilter?.filter, @@ -228,21 +256,22 @@ class ThreadRepositoryImpl extends ThreadRepository { @override Stream loadMoreEmails(GetEmailRequest emailRequest) async* { final response = await _getAllEmailsWithoutLastEmailId(emailRequest); - await _updateEmailCache(newCreated: response.emailList); + await _updateEmailCache(emailRequest.accountId, emailRequest.session.username, newCreated: response.emailList); yield response; } Future _getAllEmailsWithoutLastEmailId(GetEmailRequest emailRequest) async { final emailResponse = await mapDataSource[DataSourceType.network]! .getAllEmail( - emailRequest.accountId, - limit: emailRequest.limit, - sort: emailRequest.sort, - filter: emailRequest.filter, - properties: emailRequest.properties) + emailRequest.session, + emailRequest.accountId, + limit: emailRequest.limit, + sort: emailRequest.sort, + filter: emailRequest.filter, + properties: emailRequest.properties) .then((response) { - var listEmails = response.emailList; - return EmailsResponse(emailList: listEmails, state: response.state); + final listEmails = response.emailList; + return EmailsResponse(emailList: listEmails, state: response.state); }); return emailResponse; @@ -250,6 +279,7 @@ class ThreadRepositoryImpl extends ThreadRepository { @override Future> searchEmails( + Session session, AccountId accountId, { UnsignedInt? limit, @@ -259,6 +289,7 @@ class ThreadRepositoryImpl extends ThreadRepository { } ) async { final emailResponse = await mapDataSource[DataSourceType.network]!.getAllEmail( + session, accountId, limit: limit, sort: sort, @@ -269,17 +300,19 @@ class ThreadRepositoryImpl extends ThreadRepository { } @override - Future> emptyTrashFolder(AccountId accountId, MailboxId trashMailboxId) async { - return mapDataSource[DataSourceType.network]!.emptyTrashFolder( + Future> emptyTrashFolder(Session session, AccountId accountId, MailboxId trashMailboxId) async { + return mapDataSource[DataSourceType.network]!.emptyMailboxFolder( + session, accountId, trashMailboxId, (listEmailIdDeleted) async { - await _updateEmailCache(newDestroyed: listEmailIdDeleted); + await _updateEmailCache(accountId, session.username, newDestroyed: listEmailIdDeleted); }, ); } Future _synchronizeCacheWithChanges( + Session session, AccountId accountId, State currentState, { @@ -287,8 +320,7 @@ class ThreadRepositoryImpl extends ThreadRepository { Properties? propertiesUpdated, } ) async { - final localEmailList = await mapDataSource[DataSourceType.local]! - .getAllEmailCache(); + final localEmailList = await mapDataSource[DataSourceType.local]!.getAllEmailCache(accountId, session.username); EmailChangeResponse? emailChangeResponse; bool hasMoreChanges = true; @@ -297,10 +329,11 @@ class ThreadRepositoryImpl extends ThreadRepository { while(hasMoreChanges && sinceState != null) { log('ThreadRepositoryImpl::_synchronizeCacheWithChanges(): sinceState = $sinceState'); final changesResponse = await mapDataSource[DataSourceType.network]!.getChanges( - accountId, - sinceState, - propertiesCreated: propertiesCreated, - propertiesUpdated: propertiesUpdated); + session, + accountId, + sinceState, + propertiesCreated: propertiesCreated, + propertiesUpdated: propertiesUpdated); hasMoreChanges = changesResponse.hasMoreChanges; sinceState = changesResponse.newStateChanges; @@ -324,18 +357,41 @@ class ThreadRepositoryImpl extends ThreadRepository { 'destroyed = ${emailChangeResponse.destroyed?.length}'); await _updateEmailCache( + accountId, + session.username, newCreated: emailChangeResponse.created, newUpdated: newEmailUpdated, newDestroyed: emailChangeResponse.destroyed); if (emailChangeResponse.newStateEmail != null) { - await _updateState(emailChangeResponse.newStateEmail!); + await _updateState(accountId, session.username, emailChangeResponse.newStateEmail!); } } } @override - Future getEmailById(AccountId accountId, EmailId emailId, {Properties? properties}) { - return mapDataSource[DataSourceType.network]!.getEmailById(accountId, emailId, properties: properties); + Future getEmailById( + Session session, + AccountId accountId, + EmailId emailId, + {Properties? properties} + ) { + return mapDataSource[DataSourceType.network]!.getEmailById(session, accountId, emailId, properties: properties); + } + + @override + Future> emptySpamFolder(Session session, AccountId accountId, MailboxId spamMailboxId) { + return mapDataSource[DataSourceType.network]!.emptyMailboxFolder( + session, + accountId, + spamMailboxId, + (listEmailIdDeleted) async { + await _updateEmailCache( + accountId, + session.username, + newDestroyed: listEmailIdDeleted + ); + }, + ); } } \ No newline at end of file diff --git a/lib/features/thread/domain/constants/thread_constants.dart b/lib/features/thread/domain/constants/thread_constants.dart index b7032044f7..72c0957f76 100644 --- a/lib/features/thread/domain/constants/thread_constants.dart +++ b/lib/features/thread/domain/constants/thread_constants.dart @@ -1,5 +1,6 @@ import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:model/email/email_property.dart'; class ThreadConstants { @@ -37,4 +38,54 @@ class ThreadConstants { EmailProperty.hasAttachment, EmailProperty.mailboxIds, }); + + static final propertiesGetEmailContent = Properties({ + EmailProperty.bodyValues, + EmailProperty.htmlBody, + EmailProperty.attachments, + EmailProperty.headers, + EmailProperty.keywords, + EmailProperty.mailboxIds, + EmailProperty.messageId, + EmailProperty.references, + }); + + static final propertiesGetDetailedEmail = Properties({ + EmailProperty.id, + EmailProperty.subject, + EmailProperty.from, + EmailProperty.to, + EmailProperty.cc, + EmailProperty.bcc, + EmailProperty.keywords, + EmailProperty.size, + EmailProperty.receivedAt, + EmailProperty.sentAt, + EmailProperty.preview, + EmailProperty.hasAttachment, + EmailProperty.replyTo, + EmailProperty.mailboxIds, + EmailProperty.bodyValues, + EmailProperty.htmlBody, + EmailProperty.attachments, + EmailProperty.headers + }); + + static final propertiesCalendarEvent = Properties({ + EmailProperty.id, + EmailProperty.subject, + EmailProperty.from, + EmailProperty.to, + EmailProperty.cc, + EmailProperty.bcc, + EmailProperty.keywords, + EmailProperty.size, + EmailProperty.receivedAt, + EmailProperty.sentAt, + EmailProperty.preview, + EmailProperty.hasAttachment, + EmailProperty.replyTo, + EmailProperty.mailboxIds, + IndividualHeaderIdentifier.headerCalendarEvent.value, + }); } \ No newline at end of file diff --git a/lib/features/thread/domain/exceptions/thread_exceptions.dart b/lib/features/thread/domain/exceptions/thread_exceptions.dart new file mode 100644 index 0000000000..134c9ce636 --- /dev/null +++ b/lib/features/thread/domain/exceptions/thread_exceptions.dart @@ -0,0 +1,2 @@ + +class NotFoundEmailsDeletedException implements Exception {} \ No newline at end of file diff --git a/lib/features/thread/domain/model/get_email_request.dart b/lib/features/thread/domain/model/get_email_request.dart index 54541c0b4b..d443288c44 100644 --- a/lib/features/thread/domain/model/get_email_request.dart +++ b/lib/features/thread/domain/model/get_email_request.dart @@ -2,12 +2,14 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; class GetEmailRequest with EquatableMixin { + final Session session; final AccountId accountId; final UnsignedInt? limit; final Set? sort; @@ -16,15 +18,28 @@ class GetEmailRequest with EquatableMixin { final Properties? properties; final EmailId? lastEmailId; - GetEmailRequest(this.accountId, { - this.limit, - this.sort, - this.filter, - this.filterOption, - this.properties, - this.lastEmailId, - }); + GetEmailRequest( + this.session, + this.accountId, + { + this.limit, + this.sort, + this.filter, + this.filterOption, + this.properties, + this.lastEmailId, + } + ); @override - List get props => [limit, sort, filter, properties, lastEmailId, filterOption]; + List get props => [ + session, + accountId, + limit, + sort, + filter, + properties, + lastEmailId, + filterOption + ]; } \ No newline at end of file diff --git a/lib/features/thread/domain/repository/thread_repository.dart b/lib/features/thread/domain/repository/thread_repository.dart index f82e836cc5..90cbd17123 100644 --- a/lib/features/thread/domain/repository/thread_repository.dart +++ b/lib/features/thread/domain/repository/thread_repository.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/filter/filter.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; @@ -15,6 +16,7 @@ import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dar abstract class ThreadRepository { Stream getAllEmail( + Session session, AccountId accountId, { UnsignedInt? limit, @@ -26,6 +28,7 @@ abstract class ThreadRepository { ); Stream refreshChanges( + Session session, AccountId accountId, jmap.State currentState, { @@ -39,6 +42,7 @@ abstract class ThreadRepository { Stream loadMoreEmails(GetEmailRequest emailRequest); Future> searchEmails( + Session session, AccountId accountId, { UnsignedInt? limit, @@ -49,9 +53,21 @@ abstract class ThreadRepository { ); Future> emptyTrashFolder( - AccountId accountId, - MailboxId trashMailboxId, + Session session, + AccountId accountId, + MailboxId trashMailboxId, + ); + + Future getEmailById( + Session session, + AccountId accountId, + EmailId emailId, + {Properties? properties} ); - Future getEmailById(AccountId accountId, EmailId emailId, {Properties? properties}); + Future> emptySpamFolder( + Session session, + AccountId accountId, + MailboxId spamMailboxId, + ); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/empty_spam_folder_state.dart b/lib/features/thread/domain/state/empty_spam_folder_state.dart new file mode 100644 index 0000000000..f0bfbad412 --- /dev/null +++ b/lib/features/thread/domain/state/empty_spam_folder_state.dart @@ -0,0 +1,26 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; + +class EmptySpamFolderLoading extends LoadingState {} + +class EmptySpamFolderSuccess extends UIActionState { + + final List emailIds; + + EmptySpamFolderSuccess( + this.emailIds, { + jmap.State? currentEmailState, + jmap.State? currentMailboxState, + }) : super(currentEmailState, currentMailboxState); + + @override + List get props => [emailIds, ...super.props]; +} + +class EmptySpamFolderFailure extends FeatureFailure { + + EmptySpamFolderFailure(dynamic exception) : super(exception: exception); +} \ No newline at end of file diff --git a/lib/features/thread/domain/state/empty_trash_folder_state.dart b/lib/features/thread/domain/state/empty_trash_folder_state.dart index 6363f613ba..8f9c6b62fc 100644 --- a/lib/features/thread/domain/state/empty_trash_folder_state.dart +++ b/lib/features/thread/domain/state/empty_trash_folder_state.dart @@ -1,23 +1,26 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +class EmptyTrashFolderLoading extends LoadingState {} + class EmptyTrashFolderSuccess extends UIActionState { - EmptyTrashFolderSuccess({ - jmap.State? currentEmailState, - jmap.State? currentMailboxState, - }) : super(currentEmailState, currentMailboxState); + final List emailIds; + + EmptyTrashFolderSuccess( + this.emailIds, { + jmap.State? currentEmailState, + jmap.State? currentMailboxState, + }) : super(currentEmailState, currentMailboxState); @override - List get props => []; + List get props => [emailIds, ...super.props]; } class EmptyTrashFolderFailure extends FeatureFailure { - final dynamic exception; - EmptyTrashFolderFailure(this.exception); - - @override - List get props => [exception]; + EmptyTrashFolderFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/get_all_email_state.dart b/lib/features/thread/domain/state/get_all_email_state.dart index dd8a28fef7..dcd722b4bb 100644 --- a/lib/features/thread/domain/state/get_all_email_state.dart +++ b/lib/features/thread/domain/state/get_all_email_state.dart @@ -1,6 +1,11 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; -import 'package:model/model.dart'; +import 'package:model/email/presentation_email.dart'; + +class RefreshAllEmailLoading extends LoadingState {} + +class GetAllEmailLoading extends LoadingState {} class GetAllEmailSuccess extends UIState { final List emailList; @@ -13,10 +18,6 @@ class GetAllEmailSuccess extends UIState { } class GetAllEmailFailure extends FeatureFailure { - final dynamic exception; - GetAllEmailFailure(this.exception); - - @override - List get props => [exception]; + GetAllEmailFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/get_email_by_id_state.dart b/lib/features/thread/domain/state/get_email_by_id_state.dart index 1bca594030..4d7570b3f2 100644 --- a/lib/features/thread/domain/state/get_email_by_id_state.dart +++ b/lib/features/thread/domain/state/get_email_by_id_state.dart @@ -14,10 +14,6 @@ class GetEmailByIdSuccess extends UIState { } class GetEmailByIdFailure extends FeatureFailure { - final dynamic exception; - GetEmailByIdFailure(this.exception); - - @override - List get props => [exception]; + GetEmailByIdFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/load_more_emails_state.dart b/lib/features/thread/domain/state/load_more_emails_state.dart index 3825443bd5..074f3e881f 100644 --- a/lib/features/thread/domain/state/load_more_emails_state.dart +++ b/lib/features/thread/domain/state/load_more_emails_state.dart @@ -1,5 +1,8 @@ -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/email/presentation_email.dart'; + +class LoadingMoreEmails extends LoadingState {} class LoadMoreEmailsSuccess extends UIState { final List emailList; @@ -11,10 +14,6 @@ class LoadMoreEmailsSuccess extends UIState { } class LoadMoreEmailsFailure extends FeatureFailure { - final dynamic exception; - - LoadMoreEmailsFailure(this.exception); - @override - List get props => [exception]; + LoadMoreEmailsFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart b/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart index 5351a06ee2..1c67a8212e 100644 --- a/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart +++ b/lib/features/thread/domain/state/mark_as_multiple_email_read_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:model/email/read_actions.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; @@ -19,7 +20,7 @@ class MarkAsMultipleEmailReadAllSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [countMarkAsReadSuccess, readActions]; + List get props => [countMarkAsReadSuccess, readActions, ...super.props]; } class MarkAsMultipleEmailReadAllFailure extends FeatureFailure { @@ -45,15 +46,14 @@ class MarkAsMultipleEmailReadHasSomeEmailFailure extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [countMarkAsReadSuccess, readActions]; + List get props => [countMarkAsReadSuccess, readActions, ...super.props]; } class MarkAsMultipleEmailReadFailure extends FeatureFailure { - final dynamic exception; final ReadActions readActions; - MarkAsMultipleEmailReadFailure(this.exception, this.readActions); + MarkAsMultipleEmailReadFailure(this.readActions, dynamic exception) : super(exception: exception); @override - List get props => [exception, readActions]; + List get props => [readActions, exception]; } \ No newline at end of file diff --git a/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart b/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart index 3696a8d83c..a284194f2b 100644 --- a/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart +++ b/lib/features/thread/domain/state/mark_as_star_multiple_email_state.dart @@ -1,6 +1,7 @@ -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; +import 'package:model/email/mark_star_action.dart'; import 'package:tmail_ui_user/features/base/state/ui_action_state.dart'; class LoadingMarkAsStarMultipleEmailAll extends UIState {} @@ -19,7 +20,7 @@ class MarkAsStarMultipleEmailAllSuccess extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [countMarkStarSuccess, markStarAction]; + List get props => [countMarkStarSuccess, markStarAction, ...super.props]; } class MarkAsStarMultipleEmailAllFailure extends FeatureFailure { @@ -45,15 +46,14 @@ class MarkAsStarMultipleEmailHasSomeEmailFailure extends UIActionState { ) : super(currentEmailState, currentMailboxState); @override - List get props => [countMarkStarSuccess, markStarAction]; + List get props => [countMarkStarSuccess, markStarAction, ...super.props]; } class MarkAsStarMultipleEmailFailure extends FeatureFailure { - final dynamic exception; final MarkStarAction markStarAction; - MarkAsStarMultipleEmailFailure(this.exception, this.markStarAction); + MarkAsStarMultipleEmailFailure(this.markStarAction, dynamic exception) : super(exception: exception); @override - List get props => [exception, markStarAction]; + List get props => [markStarAction, exception]; } \ No newline at end of file diff --git a/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart b/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart index c753fbea19..283bdc464f 100644 --- a/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart +++ b/lib/features/thread/domain/state/move_multiple_email_to_mailbox_state.dart @@ -1,4 +1,5 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; @@ -36,7 +37,8 @@ class MoveMultipleEmailToMailboxAllSuccess extends UIActionState { destinationMailboxId, moveAction, emailActionType, - destinationPath + destinationPath, + ...super.props ]; } @@ -78,17 +80,17 @@ class MoveMultipleEmailToMailboxHasSomeEmailFailure extends UIActionState { destinationMailboxId, moveAction, emailActionType, - destinationPath + destinationPath, + ...super.props ]; } class MoveMultipleEmailToMailboxFailure extends FeatureFailure { - final dynamic exception; final MoveAction moveAction; final EmailActionType emailActionType; - MoveMultipleEmailToMailboxFailure(this.exception, this.emailActionType, this.moveAction); + MoveMultipleEmailToMailboxFailure(this.emailActionType, this.moveAction, dynamic exception) : super(exception: exception); @override - List get props => [exception, emailActionType, moveAction]; + List get props => [emailActionType, moveAction, exception]; } \ No newline at end of file diff --git a/lib/features/thread/domain/state/refresh_changes_all_email_state.dart b/lib/features/thread/domain/state/refresh_changes_all_email_state.dart index 296f718c93..ca5d9f3f3c 100644 --- a/lib/features/thread/domain/state/refresh_changes_all_email_state.dart +++ b/lib/features/thread/domain/state/refresh_changes_all_email_state.dart @@ -1,6 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:jmap_dart_client/jmap/core/state.dart'; -import 'package:model/model.dart'; +import 'package:model/email/presentation_email.dart'; + +class RefreshChangesAllEmailLoading extends LoadingState {} class RefreshChangesAllEmailSuccess extends UIState { final List emailList; @@ -13,10 +16,6 @@ class RefreshChangesAllEmailSuccess extends UIState { } class RefreshChangesAllEmailFailure extends FeatureFailure { - final dynamic exception; - - RefreshChangesAllEmailFailure(this.exception); - @override - List get props => [exception]; + RefreshChangesAllEmailFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/search_email_state.dart b/lib/features/thread/domain/state/search_email_state.dart index 3baf53c8c8..701b96a093 100644 --- a/lib/features/thread/domain/state/search_email_state.dart +++ b/lib/features/thread/domain/state/search_email_state.dart @@ -1,14 +1,8 @@ - -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:model/email/presentation_email.dart'; -class SearchingState extends UIState { - - SearchingState(); - - @override - List get props => []; -} +class SearchingState extends LoadingState {} class SearchEmailSuccess extends UIState { final List emailList; @@ -20,10 +14,6 @@ class SearchEmailSuccess extends UIState { } class SearchEmailFailure extends FeatureFailure { - final dynamic exception; - - SearchEmailFailure(this.exception); - @override - List get props => [exception]; + SearchEmailFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/thread/domain/state/search_more_email_state.dart b/lib/features/thread/domain/state/search_more_email_state.dart index 5a6b9342ed..7396d6bc99 100644 --- a/lib/features/thread/domain/state/search_more_email_state.dart +++ b/lib/features/thread/domain/state/search_more_email_state.dart @@ -1,15 +1,8 @@ - -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:model/email/presentation_email.dart'; -class SearchingMoreState extends UIState { - - SearchingMoreState(); - - @override - List get props => []; -} - +class SearchingMoreState extends LoadingState {} class SearchMoreEmailSuccess extends UIState { final List emailList; @@ -21,10 +14,6 @@ class SearchMoreEmailSuccess extends UIState { } class SearchMoreEmailFailure extends FeatureFailure { - final dynamic exception; - SearchMoreEmailFailure(this.exception); - - @override - List get props => [exception]; + SearchMoreEmailFailure(dynamic exception) : super(exception: exception); } \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart b/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart new file mode 100644 index 0000000000..38e864a947 --- /dev/null +++ b/lib/features/thread/domain/usecases/empty_spam_folder_interactor.dart @@ -0,0 +1,45 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; +import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; + +class EmptySpamFolderInteractor { + final ThreadRepository threadRepository; + final MailboxRepository _mailboxRepository; + final EmailRepository _emailRepository; + + EmptySpamFolderInteractor( + this.threadRepository, + this._mailboxRepository, + this._emailRepository + ); + + Stream> execute(Session session, AccountId accountId, MailboxId spamMailboxId) async* { + try { + yield Right(EmptySpamFolderLoading()); + + final listState = await Future.wait([ + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), + ], eagerError: true); + + final currentMailboxState = listState.first; + final currentEmailState = listState.last; + + final emailIdDeleted = await threadRepository.emptySpamFolder(session, accountId, spamMailboxId); + yield Right(EmptySpamFolderSuccess( + emailIdDeleted, + currentMailboxState: currentMailboxState, + currentEmailState: currentEmailState, + )); + } catch (e) { + yield Left(EmptySpamFolderFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart b/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart index d74ea460fd..4a22143ed4 100644 --- a/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart +++ b/lib/features/thread/domain/usecases/empty_trash_folder_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; @@ -18,27 +19,24 @@ class EmptyTrashFolderInteractor { this._emailRepository ); - Stream> execute(AccountId accountId, MailboxId trashMailboxId) async* { + Stream> execute(Session session, AccountId accountId, MailboxId trashMailboxId) async* { try { - yield Right(LoadingState()); + yield Right(EmptyTrashFolderLoading()); final listState = await Future.wait([ - _mailboxRepository.getMailboxState(), - _emailRepository.getEmailState(), + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), ], eagerError: true); final currentMailboxState = listState.first; final currentEmailState = listState.last; - final result = await threadRepository.emptyTrashFolder(accountId, trashMailboxId); - if (result.isNotEmpty) { - yield Right(EmptyTrashFolderSuccess( - currentMailboxState: currentMailboxState, - currentEmailState: currentEmailState, - )); - } else { - yield Left(EmptyTrashFolderFailure(null)); - } + final emailIdDeleted = await threadRepository.emptyTrashFolder(session, accountId, trashMailboxId); + yield Right(EmptyTrashFolderSuccess( + emailIdDeleted, + currentMailboxState: currentMailboxState, + currentEmailState: currentEmailState, + )); } catch (e) { yield Left(EmptyTrashFolderFailure(e)); } diff --git a/lib/features/thread/domain/usecases/get_email_by_id_interactor.dart b/lib/features/thread/domain/usecases/get_email_by_id_interactor.dart index 91c2a9f30b..65b0ccc47a 100644 --- a/lib/features/thread/domain/usecases/get_email_by_id_interactor.dart +++ b/lib/features/thread/domain/usecases/get_email_by_id_interactor.dart @@ -1,18 +1,25 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:model/extensions/email_extension.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state.dart'; class GetEmailByIdInteractor { final ThreadRepository _threadRepository; + final EmailRepository _emailRepository; - GetEmailByIdInteractor(this._threadRepository); + GetEmailByIdInteractor(this._threadRepository, this._emailRepository); Stream> execute( + Session session, AccountId accountId, EmailId emailId, { @@ -21,10 +28,49 @@ class GetEmailByIdInteractor { ) async* { try { yield Right(GetEmailByIdLoading()); - final email = await _threadRepository.getEmailById(accountId, emailId, properties: properties); + if (PlatformInfo.isMobile) { + yield* _getStoredEmail(session, accountId, emailId, properties: properties); + } else { + yield* _getEmailByIdFromServer(session, accountId, emailId, properties: properties); + } + } catch (e) { + logError('GetEmailByIdInteractor::execute():EXCEPTION: $e'); + yield Left(GetEmailByIdFailure(e)); + } + } + + Stream> _getEmailByIdFromServer( + Session session, + AccountId accountId, + EmailId emailId, + { + Properties? properties, + } + ) async* { + try { + final email = await _threadRepository.getEmailById(session, accountId, emailId, properties: properties); yield Right(GetEmailByIdSuccess(email)); } catch (e) { + logError('GetEmailByIdInteractor::_getEmailByIdFromServer():EXCEPTION: $e'); yield Left(GetEmailByIdFailure(e)); } + + } + + Stream> _getStoredEmail( + Session session, + AccountId accountId, + EmailId emailId, + { + Properties? properties, + } + ) async* { + try { + final email = await _emailRepository.getStoredEmail(session, accountId, emailId); + yield Right(GetEmailByIdSuccess(email.toPresentationEmail())); + } catch (e) { + logError('GetEmailByIdInteractor::_tryToGetEmailFromCache():EXCEPTION: $e'); + yield* _getEmailByIdFromServer(session, accountId, emailId, properties: properties); + } } } \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart b/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart index a28b1f9e84..497f982998 100644 --- a/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/get_emails_in_mailbox_interactor.dart @@ -2,6 +2,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; @@ -16,6 +17,7 @@ class GetEmailsInMailboxInteractor { GetEmailsInMailboxInteractor(this.threadRepository); Stream> execute( + Session session, AccountId accountId, { UnsignedInt? limit, @@ -26,10 +28,11 @@ class GetEmailsInMailboxInteractor { } ) async* { try { - yield Right(LoadingState()); + yield Right(GetAllEmailLoading()); yield* threadRepository .getAllEmail( + session, accountId, limit: limit, sort: sort, diff --git a/lib/features/thread/domain/usecases/load_more_emails_in_mailbox_interactor.dart b/lib/features/thread/domain/usecases/load_more_emails_in_mailbox_interactor.dart index 02ef08afc2..400e2965b3 100644 --- a/lib/features/thread/domain/usecases/load_more_emails_in_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/load_more_emails_in_mailbox_interactor.dart @@ -1,9 +1,10 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; import 'package:dartz/dartz.dart'; +import 'package:model/extensions/email_extension.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart'; import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart'; -import 'package:model/model.dart'; import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_state.dart'; class LoadMoreEmailsInMailboxInteractor { @@ -13,8 +14,7 @@ class LoadMoreEmailsInMailboxInteractor { Stream> execute(GetEmailRequest emailRequest) async* { try { - yield Right(LoadingMoreState()); - + yield Right(LoadingMoreEmails()); yield* threadRepository.loadMoreEmails(emailRequest).map(_toGetEmailState); } catch (e) { yield Left(LoadMoreEmailsFailure(e)); diff --git a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart index 0eaed2b0eb..e99da0356c 100644 --- a/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_multiple_email_read_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; @@ -14,16 +15,17 @@ class MarkAsMultipleEmailReadInteractor { MarkAsMultipleEmailReadInteractor(this._emailRepository, this._mailboxRepository); Stream> execute( - AccountId accountId, - List emails, - ReadActions readAction + Session session, + AccountId accountId, + List emails, + ReadActions readAction ) async* { try { yield Right(LoadingMarkAsMultipleEmailReadAll()); final listState = await Future.wait([ - _mailboxRepository.getMailboxState(), - _emailRepository.getEmailState(), + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), ], eagerError: true); final currentMailboxState = listState.first; @@ -33,7 +35,7 @@ class MarkAsMultipleEmailReadInteractor { .where((email) => readAction == ReadActions.markAsUnread ? email.hasRead : !email.hasRead) .toList(); - final result = await _emailRepository.markAsRead(accountId, listEmailNeedMarkAsRead, readAction); + final result = await _emailRepository.markAsRead(session, accountId, listEmailNeedMarkAsRead, readAction); if (listEmailNeedMarkAsRead.length == result.length) { final countMarkAsReadSuccess = emails.length; @@ -53,7 +55,7 @@ class MarkAsMultipleEmailReadInteractor { currentMailboxState: currentMailboxState)); } } catch (e) { - yield Left(MarkAsMultipleEmailReadFailure(e, readAction)); + yield Left(MarkAsMultipleEmailReadFailure(readAction, e)); } } } \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart index bdb0a747c6..27062847b0 100644 --- a/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart +++ b/lib/features/thread/domain/usecases/mark_as_star_multiple_email_interactor.dart @@ -1,6 +1,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; @@ -12,20 +13,21 @@ class MarkAsStarMultipleEmailInteractor { MarkAsStarMultipleEmailInteractor(this._emailRepository); Stream> execute( - AccountId accountId, - List emails, - MarkStarAction markStarAction + Session session, + AccountId accountId, + List emails, + MarkStarAction markStarAction ) async* { try { yield Right(LoadingMarkAsStarMultipleEmailAll()); - final currentEmailState = await _emailRepository.getEmailState(); + final currentEmailState = await _emailRepository.getEmailState(session, accountId); final listEmailNeedMarkStar = emails .where((email) => markStarAction == MarkStarAction.unMarkStar ? email.hasStarred : !email.hasStarred) .toList(); - final result = await _emailRepository.markAsStar(accountId, listEmailNeedMarkStar, markStarAction); + final result = await _emailRepository.markAsStar(session, accountId, listEmailNeedMarkStar, markStarAction); if (listEmailNeedMarkStar.length == result.length) { final countMarkStarSuccess = emails.length; @@ -43,7 +45,7 @@ class MarkAsStarMultipleEmailInteractor { currentEmailState: currentEmailState)); } } catch (e) { - yield Left(MarkAsStarMultipleEmailFailure(e, markStarAction)); + yield Left(MarkAsStarMultipleEmailFailure(markStarAction, e)); } } } \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart index 342b221bee..0069c1b9ec 100644 --- a/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/move_multiple_email_to_mailbox_interactor.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox/domain/repository/mailbox_repository.dart'; @@ -14,19 +15,23 @@ class MoveMultipleEmailToMailboxInteractor { MoveMultipleEmailToMailboxInteractor(this._emailRepository, this._mailboxRepository); - Stream> execute(AccountId accountId, MoveToMailboxRequest moveRequest) async* { + Stream> execute( + Session session, + AccountId accountId, + MoveToMailboxRequest moveRequest + ) async* { try { yield Right(LoadingMoveMultipleEmailToMailboxAll()); final listState = await Future.wait([ - _mailboxRepository.getMailboxState(), - _emailRepository.getEmailState(), + _mailboxRepository.getMailboxState(session, accountId), + _emailRepository.getEmailState(session, accountId), ], eagerError: true); final currentMailboxState = listState.first; final currentEmailState = listState.last; - final result = await _emailRepository.moveToMailbox(accountId, moveRequest); + final result = await _emailRepository.moveToMailbox(session, accountId, moveRequest); int totalEmail = 0; for (var element in moveRequest.currentMailboxes.values) { totalEmail = totalEmail + element.length; @@ -54,7 +59,7 @@ class MoveMultipleEmailToMailboxInteractor { currentMailboxState: currentMailboxState)); } } catch (e) { - yield Left(MoveMultipleEmailToMailboxFailure(e, moveRequest.emailActionType, moveRequest.moveAction)); + yield Left(MoveMultipleEmailToMailboxFailure(moveRequest.emailActionType, moveRequest.moveAction, e)); } } } \ No newline at end of file diff --git a/lib/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor.dart b/lib/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor.dart index 73a75689ce..96623df70d 100644 --- a/lib/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor.dart +++ b/lib/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor.dart @@ -2,6 +2,7 @@ import 'package:core/core.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart'; @@ -16,6 +17,7 @@ class RefreshChangesEmailsInMailboxInteractor { RefreshChangesEmailsInMailboxInteractor(this.threadRepository); Stream> execute( + Session session, AccountId accountId, jmap.State currentState, { @@ -25,11 +27,12 @@ class RefreshChangesEmailsInMailboxInteractor { EmailFilter? emailFilter, } ) async* { - yield Right(RefreshingState()); + yield Right(RefreshChangesAllEmailLoading()); try { yield* threadRepository .refreshChanges( + session, accountId, currentState, sort: sort, diff --git a/lib/features/thread/domain/usecases/search_email_interactor.dart b/lib/features/thread/domain/usecases/search_email_interactor.dart index ab9515bfb9..a84318bb8b 100644 --- a/lib/features/thread/domain/usecases/search_email_interactor.dart +++ b/lib/features/thread/domain/usecases/search_email_interactor.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:model/model.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; @@ -17,6 +18,7 @@ class SearchEmailInteractor { SearchEmailInteractor(this.threadRepository); Stream> execute( + Session session, AccountId accountId, { UnsignedInt? limit, @@ -29,6 +31,7 @@ class SearchEmailInteractor { yield Right(SearchingState()); final emailList = await threadRepository.searchEmails( + session, accountId, limit: limit, sort: sort, diff --git a/lib/features/thread/domain/usecases/search_more_email_interactor.dart b/lib/features/thread/domain/usecases/search_more_email_interactor.dart index 06ea63da09..c25ef39f5b 100644 --- a/lib/features/thread/domain/usecases/search_more_email_interactor.dart +++ b/lib/features/thread/domain/usecases/search_more_email_interactor.dart @@ -1,5 +1,6 @@ import 'package:core/core.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; import 'package:dartz/dartz.dart'; @@ -18,6 +19,7 @@ class SearchMoreEmailInteractor { SearchMoreEmailInteractor(this.threadRepository); Stream> execute( + Session session, AccountId accountId, { UnsignedInt? limit, @@ -31,6 +33,7 @@ class SearchMoreEmailInteractor { yield Right(SearchingMoreState()); final emailList = await threadRepository.searchEmails( + session, accountId, limit: limit, sort: sort, diff --git a/lib/features/thread/presentation/extensions/list_presentation_email_extensions.dart b/lib/features/thread/presentation/extensions/list_presentation_email_extensions.dart index 5a4a441106..c88e214498 100644 --- a/lib/features/thread/presentation/extensions/list_presentation_email_extensions.dart +++ b/lib/features/thread/presentation/extensions/list_presentation_email_extensions.dart @@ -1,5 +1,5 @@ -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/presentation_email_extension.dart'; @@ -41,7 +41,7 @@ extension ListPresentationEmailExtensions on List { bool isSearchEmailRunning = false, SearchQuery? searchQuery, }) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { final route = RouteUtils.generateRouteBrowser( AppRoutes.dashboard, NavigationRouter( diff --git a/lib/features/thread/presentation/mixin/base_email_item_tile.dart b/lib/features/thread/presentation/mixin/base_email_item_tile.dart index d1ad353496..33021a622e 100644 --- a/lib/features/thread/presentation/mixin/base_email_item_tile.dart +++ b/lib/features/thread/presentation/mixin/base_email_item_tile.dart @@ -1,19 +1,26 @@ import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/extensions/string_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/utils/style_utils.dart'; import 'package:core/presentation/views/text/rich_text_builder.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/presentation/views/text/text_overflow_builder.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/extensions/presentation_email_extension.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/item_email_tile_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnPressEmailActionClick = void Function(EmailActionType, PresentationEmail); typedef OnMoreActionClick = void Function(PresentationEmail, RelativeRect?); @@ -24,24 +31,22 @@ mixin BaseEmailItemTile { final imagePaths = Get.find(); Widget buildMailboxContain( + BuildContext context, bool isSearchEmailRunning, PresentationEmail email ) { if (hasMailboxLabel(isSearchEmailRunning, email)) { return Container( - margin: const EdgeInsets.only(left: 8), - padding: const EdgeInsets.symmetric( + margin: const EdgeInsetsDirectional.only(start: 8), + padding: const EdgeInsetsDirectional.symmetric( horizontal: 8, vertical: 3), constraints: const BoxConstraints(maxWidth: 100), decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: AppColor.backgroundCounterMailboxColor), - child: Text( - email.mailboxName, - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, + child: TextOverflowBuilder( + email.mailboxContain?.getDisplayName(context) ?? '', style: const TextStyle( fontSize: 10, color: AppColor.mailboxTextColor, @@ -64,7 +69,7 @@ mixin BaseEmailItemTile { email.hasRead ? AppColor.colorContentEmail : AppColor.colorNameEmail; bool hasMailboxLabel(bool isSearchEmailRunning, PresentationEmail email) { - return isSearchEmailRunning && email.mailboxName.isNotEmpty; + return isSearchEmailRunning && email.mailboxContain != null; } String informationSender(PresentationEmail email, PresentationMailbox? mailbox) { @@ -76,14 +81,17 @@ mixin BaseEmailItemTile { } Widget buildInformationSender( - PresentationEmail email, - PresentationMailbox? mailbox, - bool isSearchEmailRunning, - SearchQuery? query + BuildContext context, + PresentationEmail email, + PresentationMailbox? mailbox, + bool isSearchEmailRunning, + SearchQuery? query ) { if (isSearchEnabled(isSearchEmailRunning, query)) { return RichTextBuilder( - informationSender(email, mailbox), + DirectionUtils.isDirectionRTLByLanguage(context) + ? informationSender(email, mailbox) + : informationSender(email, mailbox).overflow, query?.value ?? '', TextStyle( fontSize: 15, @@ -96,28 +104,28 @@ mixin BaseEmailItemTile { fontWeight: buildFontForReadEmail(email)) ).build(); } else { - return Text( - informationSender(email, mailbox), - softWrap: CommonTextStyle.defaultSoftWrap, + return TextOverflowBuilder( + informationSender(email, mailbox), + style: TextStyle( + fontSize: 15, overflow: CommonTextStyle.defaultTextOverFlow, - maxLines: 1, - style: TextStyle( - fontSize: 15, - overflow: CommonTextStyle.defaultTextOverFlow, - color: buildTextColorForReadEmail(email), - fontWeight: buildFontForReadEmail(email)) + color: buildTextColorForReadEmail(email), + fontWeight: buildFontForReadEmail(email)) ); } } Widget buildEmailTitle( + BuildContext context, PresentationEmail email, bool isSearchEmailRunning, SearchQuery? query ) { if (isSearchEnabled(isSearchEmailRunning, query)) { return RichTextBuilder( - email.getEmailTitle(), + DirectionUtils.isDirectionRTLByLanguage(context) + ? email.getEmailTitle() + : email.getEmailTitle().overflow, query?.value ?? '', TextStyle( fontSize: 13, @@ -130,27 +138,27 @@ mixin BaseEmailItemTile { fontWeight: buildFontForReadEmail(email)) ).build(); } else { - return Text( - email.getEmailTitle(), - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - maxLines: 1, - style: TextStyle( - fontSize: 13, - color: buildTextColorForReadEmail(email), - fontWeight: buildFontForReadEmail(email)) + return TextOverflowBuilder( + email.getEmailTitle(), + style: TextStyle( + fontSize: 13, + color: buildTextColorForReadEmail(email), + fontWeight: buildFontForReadEmail(email)) ); } } Widget buildEmailPartialContent( + BuildContext context, PresentationEmail email, bool isSearchEmailRunning, SearchQuery? query ) { if (isSearchEnabled(isSearchEmailRunning, query)) { return RichTextBuilder( - email.getPartialContent(), + DirectionUtils.isDirectionRTLByLanguage(context) + ? email.getPartialContent() + : email.getPartialContent().overflow, query?.value ?? '', const TextStyle( fontSize: 13, @@ -162,15 +170,12 @@ mixin BaseEmailItemTile { backgroundColor: AppColor.bgWordSearch) ).build(); } else { - return Text( - email.getPartialContent(), - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: const TextStyle( - fontSize: 13, - color: AppColor.colorContentEmail, - fontWeight: FontWeight.normal) + return TextOverflowBuilder( + email.getPartialContent(), + style: const TextStyle( + fontSize: 13, + color: AppColor.colorContentEmail, + fontWeight: FontWeight.normal) ); } } @@ -248,15 +253,15 @@ mixin BaseEmailItemTile { TextStyle? textStyle } ) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return Container( color: Colors.transparent, width: iconSize ?? 48, height: iconSize ?? 48, alignment: Alignment.center, padding: responsiveUtils.isDesktop(context) - ? const EdgeInsets.symmetric(horizontal: 4) - : const EdgeInsets.all(12), + ? const EdgeInsetsDirectional.symmetric(horizontal: 4) + : const EdgeInsetsDirectional.all(12), child: SvgPicture.asset( email.selectMode == SelectMode.ACTIVE ? imagePaths.icSelected @@ -274,4 +279,75 @@ mixin BaseEmailItemTile { width: 24, height: 24)); } } + + Widget buildIconAnsweredOrForwarded({ + required PresentationEmail presentationEmail, + double? width, + double? height + }) { + if (presentationEmail.isAnsweredAndForwarded) { + return _iconAnsweredOrForwardedWidget( + iconPath: imagePaths.icReplyAndForward, + width: width, + height: height + ); + } else if (presentationEmail.isAnswered) { + return _iconAnsweredOrForwardedWidget( + iconPath: imagePaths.icReply, + width: width, + height: height + ); + } else if (presentationEmail.isForwarded) { + return _iconAnsweredOrForwardedWidget( + iconPath: imagePaths.icForwarded, + width: width, + height: height + ); + } else { + return const SizedBox(width: 16, height: 16); + } + } + + Widget _iconAnsweredOrForwardedWidget({ + required String iconPath, + double? width, + double? height + }) { + return SvgPicture.asset( + iconPath, + width: width ?? 20, + height: height ?? 20, + colorFilter: AppColor.colorAttachmentIcon.asFilter(), + fit: BoxFit.fill); + } + + String? messageToolTipForAnsweredOrForwarded(BuildContext context, PresentationEmail presentationEmail) { + if (presentationEmail.isAnsweredAndForwarded) { + return AppLocalizations.of(context).repliedAndForwardedMessage; + } else if (presentationEmail.isAnswered) { + return AppLocalizations.of(context).repliedMessage; + } else if (presentationEmail.isForwarded){ + return AppLocalizations.of(context).forwardedMessage; + } else { + return null; + } + } + + Widget buildCalendarEventIcon({ + required BuildContext context, + required PresentationEmail presentationEmail + }) { + return Padding( + padding: ItemEmailTileStyles.getSpaceCalendarEventIcon(context, responsiveUtils), + child: SvgPicture.asset( + imagePaths.icCalendarEvent, + width: 20, + height: 20, + fit: BoxFit.fill, + colorFilter: presentationEmail.hasRead + ? AppColor.colorCalendarEventRead.asFilter() + : AppColor.colorCalendarEventUnread.asFilter(), + ), + ); + } } \ No newline at end of file diff --git a/lib/features/thread/presentation/mixin/email_action_controller.dart b/lib/features/thread/presentation/mixin/email_action_controller.dart index cae986fd01..ce81b3c068 100644 --- a/lib/features/thread/presentation/mixin/email_action_controller.dart +++ b/lib/features/thread/presentation/mixin/email_action_controller.dart @@ -5,7 +5,7 @@ import 'package:core/presentation/utils/responsive_utils.dart'; import 'package:core/presentation/views/bottom_popup/confirmation_dialog_action_sheet_builder.dart'; import 'package:core/presentation/views/dialog/confirmation_dialog_builder.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; @@ -15,10 +15,11 @@ import 'package:model/email/email_action_type.dart'; import 'package:model/email/mark_star_action.dart'; import 'package:model/email/presentation_email.dart'; import 'package:model/email/read_actions.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:pointer_interceptor/pointer_interceptor.dart'; -import 'package:tmail_ui_user/features/base/mixin/view_as_dialog_action_mixin.dart'; import 'package:tmail_ui_user/features/destination_picker/presentation/model/destination_picker_arguments.dart'; +import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_action.dart'; import 'package:tmail_ui_user/features/email/domain/model/move_to_mailbox_request.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; @@ -27,22 +28,18 @@ import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; -mixin EmailActionController on ViewAsDialogActionMixin { +mixin EmailActionController { final mailboxDashBoardController = Get.find(); final responsiveUtils = Get.find(); final imagePaths = Get.find(); - void editEmail(PresentationEmail presentationEmail) { - final arguments = ComposerArguments( - emailActionType: EmailActionType.edit, - presentationEmail: presentationEmail, - mailboxRole: mailboxDashBoardController.selectedMailbox.value?.role); - - mailboxDashBoardController.goToComposer(arguments); + void editDraftEmail(PresentationEmail presentationEmail) { + mailboxDashBoardController.goToComposer(ComposerArguments.editDraftEmail(presentationEmail)); } void previewEmail(PresentationEmail presentationEmail) { @@ -51,57 +48,69 @@ mixin EmailActionController on ViewAsDialogActionMixin { } void moveToTrash(PresentationEmail email, {PresentationMailbox? mailboxContain}) async { + final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; final trashMailboxId = mailboxDashBoardController.mapDefaultMailboxIdByRole[PresentationMailbox.roleTrash]; - if (mailboxContain != null && accountId != null && trashMailboxId != null) { - _moveToTrashAction(accountId, MoveToMailboxRequest( - {mailboxContain.id: [email.id]}, - trashMailboxId, - MoveAction.moving, - mailboxDashBoardController.sessionCurrent!, - EmailActionType.moveToTrash) + if (session != null && mailboxContain != null && accountId != null && trashMailboxId != null) { + _moveToTrashAction( + session, + accountId, + MoveToMailboxRequest( + {mailboxContain.id: email.id != null ? [email.id!] : []}, + trashMailboxId, + MoveAction.moving, + mailboxDashBoardController.sessionCurrent!, + EmailActionType.moveToTrash) ); } } - void _moveToTrashAction(AccountId accountId, MoveToMailboxRequest moveRequest) { - mailboxDashBoardController.moveToMailbox(accountId, moveRequest); + void _moveToTrashAction(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { + mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); } void moveToSpam(PresentationEmail email, {PresentationMailbox? mailboxContain}) async { + final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; final spamMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleSpam); - if (mailboxContain != null && accountId != null && spamMailboxId != null) { - moveToSpamAction(accountId, MoveToMailboxRequest( - {mailboxContain.id: [email.id]}, - spamMailboxId, - MoveAction.moving, - mailboxDashBoardController.sessionCurrent!, - EmailActionType.moveToSpam) + if (session != null && mailboxContain != null && accountId != null && spamMailboxId != null) { + moveToSpamAction( + session, + accountId, + MoveToMailboxRequest( + {mailboxContain.id: email.id != null ? [email.id!] : []}, + spamMailboxId, + MoveAction.moving, + mailboxDashBoardController.sessionCurrent!, + EmailActionType.moveToSpam) ); } } void unSpam(PresentationEmail email) async { + final session = mailboxDashBoardController.sessionCurrent; final accountId = mailboxDashBoardController.accountId.value; final spamMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleSpam); final inboxMailboxId = mailboxDashBoardController.getMailboxIdByRole(PresentationMailbox.roleInbox); - if (inboxMailboxId != null && accountId != null && spamMailboxId != null) { - moveToSpamAction(accountId, MoveToMailboxRequest( - {spamMailboxId: [email.id]}, - inboxMailboxId, - MoveAction.moving, - mailboxDashBoardController.sessionCurrent!, - EmailActionType.unSpam) + if (session != null && inboxMailboxId != null && accountId != null && spamMailboxId != null) { + moveToSpamAction( + session, + accountId, + MoveToMailboxRequest( + {spamMailboxId: email.id != null ? [email.id!] : []}, + inboxMailboxId, + MoveAction.moving, + mailboxDashBoardController.sessionCurrent!, + EmailActionType.unSpam) ); } } - void moveToSpamAction(AccountId accountId, MoveToMailboxRequest moveRequest) { - mailboxDashBoardController.moveToMailbox(accountId, moveRequest); + void moveToSpamAction(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { + mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); } void moveToMailbox( @@ -110,42 +119,31 @@ mixin EmailActionController on ViewAsDialogActionMixin { {PresentationMailbox? mailboxContain} ) async { final accountId = mailboxDashBoardController.accountId.value; - final _session = mailboxDashBoardController.sessionCurrent; + final session = mailboxDashBoardController.sessionCurrent; if (mailboxContain != null && accountId != null) { - final arguments = DestinationPickerArguments(accountId, MailboxActions.moveEmail, _session); + final arguments = DestinationPickerArguments( + accountId, + MailboxActions.moveEmail, + session, + mailboxIdSelected: mailboxContain.mailboxId); - if (BuildUtils.isWeb) { - showDialogDestinationPicker( - context: context, - arguments: arguments, - onSelectedMailbox: (destinationMailbox) { - if (mailboxDashBoardController.sessionCurrent != null) { - _dispatchMoveToAction( - context, - accountId, - mailboxDashBoardController.sessionCurrent!, - email, - mailboxContain, - destinationMailbox); - } - }); - } else { - final destinationMailbox = await push( - AppRoutes.destinationPicker, - arguments: arguments); + final destinationMailbox = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.destinationPicker, arguments: arguments) + : await push(AppRoutes.destinationPicker, arguments: arguments); - if (destinationMailbox != null && + if (destinationMailbox != null && + context.mounted && destinationMailbox is PresentationMailbox && - mailboxDashBoardController.sessionCurrent != null) { - _dispatchMoveToAction( - context, - accountId, - mailboxDashBoardController.sessionCurrent!, - email, - mailboxContain, - destinationMailbox); - } + mailboxDashBoardController.sessionCurrent != null + ) { + _dispatchMoveToAction( + context, + accountId, + mailboxDashBoardController.sessionCurrent!, + email, + mailboxContain, + destinationMailbox); } } } @@ -159,32 +157,41 @@ mixin EmailActionController on ViewAsDialogActionMixin { PresentationMailbox destinationMailbox ) { if (destinationMailbox.isTrash) { - moveToSpamAction(accountId, MoveToMailboxRequest( - {currentMailbox.id: [emailSelected.id]}, - destinationMailbox.id, - MoveAction.moving, + moveToSpamAction( session, - EmailActionType.moveToTrash)); + accountId, + MoveToMailboxRequest( + {currentMailbox.id: emailSelected.id != null ? [emailSelected.id!] : []}, + destinationMailbox.id, + MoveAction.moving, + session, + EmailActionType.moveToTrash)); } else if (destinationMailbox.isSpam) { - moveToSpamAction(accountId, MoveToMailboxRequest( - {currentMailbox.id: [emailSelected.id]}, - destinationMailbox.id, - MoveAction.moving, + moveToSpamAction( session, - EmailActionType.moveToSpam)); + accountId, + MoveToMailboxRequest( + {currentMailbox.id: emailSelected.id != null ? [emailSelected.id!] : []}, + destinationMailbox.id, + MoveAction.moving, + session, + EmailActionType.moveToSpam)); } else { - _moveToMailboxAction(accountId, MoveToMailboxRequest( - {currentMailbox.id: [emailSelected.id]}, - destinationMailbox.id, - MoveAction.moving, + _moveToMailboxAction( session, - EmailActionType.moveToMailbox, - destinationPath: destinationMailbox.mailboxPath)); + accountId, + MoveToMailboxRequest( + {currentMailbox.id: emailSelected.id != null ? [emailSelected.id!] : []}, + destinationMailbox.id, + MoveAction.moving, + session, + EmailActionType.moveToMailbox, + destinationPath: destinationMailbox.mailboxPath)); } } - void _moveToMailboxAction(AccountId accountId, MoveToMailboxRequest moveRequest) { - mailboxDashBoardController.moveToMailbox(accountId, moveRequest); + void _moveToMailboxAction(Session session, AccountId accountId, MoveToMailboxRequest moveRequest) { + mailboxDashBoardController.moveToMailbox(session, accountId, moveRequest); } void deleteEmailPermanently(BuildContext context, PresentationEmail email) { @@ -227,8 +234,8 @@ mixin EmailActionController on ViewAsDialogActionMixin { mailboxDashBoardController.deleteEmailPermanently(email); } - void markAsEmailRead(PresentationEmail presentationEmail, ReadActions readActions) async { - mailboxDashBoardController.markAsEmailRead(presentationEmail, readActions); + void markAsEmailRead(PresentationEmail presentationEmail, ReadActions readActions, MarkReadAction markReadAction) async { + mailboxDashBoardController.markAsEmailRead(presentationEmail, readActions, markReadAction); } void markAsStarEmail(PresentationEmail presentationEmail, MarkStarAction action) { diff --git a/lib/features/thread/presentation/model/loading_more_status.dart b/lib/features/thread/presentation/model/loading_more_status.dart new file mode 100644 index 0000000000..823362b9ed --- /dev/null +++ b/lib/features/thread/presentation/model/loading_more_status.dart @@ -0,0 +1,8 @@ + +enum LoadingMoreStatus { + idle, + running, + completed; + + bool get isRunning => this == LoadingMoreStatus.running; +} \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/app_bar/default_web_app_bar_thread_widget_style.dart b/lib/features/thread/presentation/styles/app_bar/default_web_app_bar_thread_widget_style.dart new file mode 100644 index 0000000000..4d7cf4b1dc --- /dev/null +++ b/lib/features/thread/presentation/styles/app_bar/default_web_app_bar_thread_widget_style.dart @@ -0,0 +1,28 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; + +class DefaultWebAppBarThreadWidgetStyle { + static const double buttonMaxWidth = 80; + static const double titleOffset = 180; + static const double minHeight = 56; + + static const Color backgroundColor = Colors.white; + + static const EdgeInsetsGeometry padding = EdgeInsets.symmetric(vertical: 8, horizontal: 16); + static const EdgeInsetsGeometry mailboxMenuPadding = EdgeInsets.all(5); + static const EdgeInsetsGeometry titlePadding = EdgeInsets.symmetric(horizontal: 16); + + static const TextStyle titleTextStyle = TextStyle( + fontSize: 21, + color: Colors.black, + fontWeight: FontWeight.bold + ); + + static Color getFilterButtonColor(FilterMessageOption option) { + return option == FilterMessageOption.all + ? AppColor.colorFilterMessageDisabled + : AppColor.colorFilterMessageEnabled; + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/app_bar/mobile_app_bar_thread_widget_style.dart b/lib/features/thread/presentation/styles/app_bar/mobile_app_bar_thread_widget_style.dart new file mode 100644 index 0000000000..593e247ab8 --- /dev/null +++ b/lib/features/thread/presentation/styles/app_bar/mobile_app_bar_thread_widget_style.dart @@ -0,0 +1,30 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; + +class MobileAppBarThreadWidgetStyle { + static const double buttonMaxWidth = 80; + static const double titleOffset = 180; + static const double minHeight = 56; + + static const Color backgroundColor = Colors.white; + static const Color backButtonColor = AppColor.primaryColor; + + static const EdgeInsetsGeometry padding = EdgeInsets.symmetric(vertical: 8, horizontal: 16); + + static const TextStyle editButtonStyle = TextStyle( + fontSize: 17, + color: AppColor.primaryColor + ); + static const TextStyle emailCounterTitleStyle = TextStyle( + fontSize: 17, + color: AppColor.primaryColor + ); + + static Color getFilterButtonColor(FilterMessageOption option) { + return option == FilterMessageOption.all + ? AppColor.colorFilterMessageDisabled + : AppColor.colorFilterMessageEnabled; + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/app_bar/selection_web_app_bar_thread_widget_style.dart b/lib/features/thread/presentation/styles/app_bar/selection_web_app_bar_thread_widget_style.dart new file mode 100644 index 0000000000..f4f3d4218f --- /dev/null +++ b/lib/features/thread/presentation/styles/app_bar/selection_web_app_bar_thread_widget_style.dart @@ -0,0 +1,28 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class SelectionWebAppBarThreadWidgetStyle { + static const double minHeight = 56; + static const double iconSize = 20; + static const double mediumIconSize = 22; + static const double closeButtonIconSize = 28; + + static const Color backgroundColor = Colors.white; + static const Color cancelButtonColor = AppColor.primaryColor; + + static const EdgeInsetsGeometry padding = EdgeInsets.symmetric(vertical: 8, horizontal: 16); + static const EdgeInsetsGeometry closeButtonPadding = EdgeInsets.all(3); + + static const TextStyle emailCounterStyle = TextStyle( + fontSize: 17, + fontWeight: FontWeight.w500, + color: AppColor.primaryColor + ); + + static Color getDeleteButtonColor(bool deletePermanentlyValid) { + return deletePermanentlyValid + ? AppColor.colorDeletePermanentlyButton + : AppColor.primaryColor; + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/app_bar/title_app_bar_thread_widget_style.dart b/lib/features/thread/presentation/styles/app_bar/title_app_bar_thread_widget_style.dart new file mode 100644 index 0000000000..3914f02f7e --- /dev/null +++ b/lib/features/thread/presentation/styles/app_bar/title_app_bar_thread_widget_style.dart @@ -0,0 +1,17 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class TitleAppBarThreadWidgetStyle { + static const EdgeInsetsGeometry padding = EdgeInsets.symmetric(vertical: 8, horizontal: 16); + + static const TextStyle titleStyle = TextStyle( + fontSize: 21, + color: Colors.black, + fontWeight: FontWeight.w700 + ); + static const TextStyle filterOptionStyle = TextStyle( + fontSize: 11, + color: AppColor.colorContentEmail + ); +} \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/banner_delete_all_spam_emails_styles.dart b/lib/features/thread/presentation/styles/banner_delete_all_spam_emails_styles.dart new file mode 100644 index 0000000000..6535b5e1ca --- /dev/null +++ b/lib/features/thread/presentation/styles/banner_delete_all_spam_emails_styles.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class BannerDeleteAllSpamEmailsStyles { + static const double borderRadius = 14; + static const double buttonBorderRadius = 20; + static const double horizontalPadding = 16; + static const double verticalPadding = 10; + static const double webEndMargin = 16; + static const double webTopMargin = 8; + static const double mobileMargin = 16; + static const double labelTextSize = 13; + static const double buttonTextSize = 17; + static const double iconSize = 20; + static const double space = 8; + static const Color backgroundColor = Colors.white; + static const Color borderStrokeColor = AppColor.colorBorderBodyThread; + static const Color labelTextColor = AppColor.colorSubtitle; + static const Color buttonTextColor = AppColor.toastErrorBackgroundColor; +} \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/banner_empty_trash_styles.dart b/lib/features/thread/presentation/styles/banner_empty_trash_styles.dart new file mode 100644 index 0000000000..fd7704734f --- /dev/null +++ b/lib/features/thread/presentation/styles/banner_empty_trash_styles.dart @@ -0,0 +1,21 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class BannerEmptyTrashStyles { + static const double borderRadius = 14; + static const double buttonBorderRadius = 20; + static const double horizontalPadding = 16; + static const double verticalPadding = 10; + static const double webEndMargin = 16; + static const double webTopMargin = 8; + static const double mobileMargin = 16; + static const double labelTextSize = 13; + static const double buttonTextSize = 17; + static const double iconSize = 20; + static const double space = 8; + static const Color backgroundColor = Colors.white; + static const Color borderStrokeColor = AppColor.colorBorderBodyThread; + static const Color labelTextColor = AppColor.colorSubtitle; + static const Color buttonTextColor = AppColor.toastErrorBackgroundColor; +} \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/empty_emails_widget_styles.dart b/lib/features/thread/presentation/styles/empty_emails_widget_styles.dart new file mode 100644 index 0000000000..da0e79710e --- /dev/null +++ b/lib/features/thread/presentation/styles/empty_emails_widget_styles.dart @@ -0,0 +1,30 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class EmptyEmailsWidgetStyles { + static const double mobileIconSize = 160; + static const double tabletIconSize = 180; + static const double desktopIconSize = 212; + static const double labelTextSize = 20; + static const double messageTextSize = 15; + static const double createFilterLabelTextSize = 22; + static const double createFilterButtonTextSize = 17; + static const double maxWidth = 490; + static const double createFilterButtonBorderRadius = 10; + + static const FontWeight labelFontWeight = FontWeight.w400; + static const FontWeight messageFontWeight = FontWeight.w400; + static const FontWeight createFilterLabelFontWeight = FontWeight.w600; + static const FontWeight createFilterButtonFontWeight = FontWeight.w500; + + static const Color labelTextColor = Colors.black; + static const Color messageTextColor = AppColor.colorSubtitle; + static const Color createFilterButtonTextColor = AppColor.primaryColor; + static const Color createFilterButtonBackgroundColor = AppColor.colorCreateFiltersButton; + + static const EdgeInsetsGeometry padding = EdgeInsetsDirectional.all(16); + static const EdgeInsetsGeometry labelPadding = EdgeInsetsDirectional.symmetric(vertical: 12); + static const EdgeInsetsGeometry createFilterButtonPadding = EdgeInsetsDirectional.symmetric(vertical: 12, horizontal: 24); + static const EdgeInsetsGeometry createFilterButtonMargin = EdgeInsetsDirectional.only(top: 28); +} \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/item_email_tile_styles.dart b/lib/features/thread/presentation/styles/item_email_tile_styles.dart new file mode 100644 index 0000000000..2c2c6c83f6 --- /dev/null +++ b/lib/features/thread/presentation/styles/item_email_tile_styles.dart @@ -0,0 +1,16 @@ + +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/cupertino.dart'; + +class ItemEmailTileStyles { + + static EdgeInsetsGeometry getSpaceCalendarEventIcon(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isScreenWithShortestSide(context)) { + return const EdgeInsetsDirectional.only(end: 4); + } else if (responsiveUtils.isWebDesktop(context)) { + return const EdgeInsetsDirectional.only(end: 12); + } else { + return const EdgeInsetsDirectional.only(end: 8); + } + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/spam_banner/spam_report_banner_button_styles.dart b/lib/features/thread/presentation/styles/spam_banner/spam_report_banner_button_styles.dart new file mode 100644 index 0000000000..1f4be80ff5 --- /dev/null +++ b/lib/features/thread/presentation/styles/spam_banner/spam_report_banner_button_styles.dart @@ -0,0 +1,14 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class SpamReportBannerButtonStyles { + static const double padding = 8; + static const double paddingIcon = 4; + static const double borderRadius = 10; + static const double labelTextSize = 16; + static const double iconSize = 20; + static const Color backgroundColor = AppColor.colorSpamReportBannerButtonBackground; + static const Color positiveButtonTextColor = AppColor.primaryColor; + static const Color negativeButtonTextColor = AppColor.textFieldErrorBorderColor; +} \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/spam_banner/spam_report_banner_label_styles.dart b/lib/features/thread/presentation/styles/spam_banner/spam_report_banner_label_styles.dart new file mode 100644 index 0000000000..029522b474 --- /dev/null +++ b/lib/features/thread/presentation/styles/spam_banner/spam_report_banner_label_styles.dart @@ -0,0 +1,11 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class SpamReportBannerLabelStyles { + static const double iconSize = 24; + static const double labelTextSize = 16; + static const double space = 8; + static const Color labelTextColor = AppColor.colorSpamReportBannerLabelColor; + static const Color highlightLabelTextColor = AppColor.primaryColor; +} \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/spam_banner/spam_report_banner_styles.dart b/lib/features/thread/presentation/styles/spam_banner/spam_report_banner_styles.dart new file mode 100644 index 0000000000..50d506e7e0 --- /dev/null +++ b/lib/features/thread/presentation/styles/spam_banner/spam_report_banner_styles.dart @@ -0,0 +1,13 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class SpamReportBannerStyles { + static const double horizontalMargin = 16; + static const double verticalMargin = 8; + static const double padding = 12; + static const double borderRadius = 12; + static const double space = 8; + static Color backgroundColor = AppColor.colorSpamReportBannerBackground.withOpacity(0.12); + static const Color strokeBorderColor = AppColor.colorSpamReportBannerStrokeBorder; +} \ No newline at end of file diff --git a/lib/features/thread/presentation/styles/spam_banner/spam_report_banner_web_styles.dart b/lib/features/thread/presentation/styles/spam_banner/spam_report_banner_web_styles.dart new file mode 100644 index 0000000000..99718b6019 --- /dev/null +++ b/lib/features/thread/presentation/styles/spam_banner/spam_report_banner_web_styles.dart @@ -0,0 +1,13 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:flutter/material.dart'; + +class SpamReportBannerWebStyles { + static const double horizontalMargin = 16; + static const double verticalMargin = 16; + static const double verticalPadding = 8; + static const double horizontalPadding = 16; + static const double borderRadius = 12; + static Color backgroundColor = AppColor.colorSpamReportBannerBackground.withOpacity(0.12); + static const Color strokeBorderColor = AppColor.colorSpamReportBannerStrokeBorder; +} \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_bindings.dart b/lib/features/thread/presentation/thread_bindings.dart index 4f5288245d..8a357b001a 100644 --- a/lib/features/thread/presentation/thread_bindings.dart +++ b/lib/features/thread/presentation/thread_bindings.dart @@ -1,8 +1,8 @@ import 'package:core/data/model/source_type/data_source_type.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/caching/caching_manager.dart'; -import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; +import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource/state_datasource.dart'; import 'package:tmail_ui_user/features/mailbox/data/datasource_impl/state_datasource_impl.dart'; import 'package:tmail_ui_user/features/thread/data/datasource/thread_datasource.dart'; @@ -33,7 +33,6 @@ class ThreadBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), - Get.find(), Get.find(), )); } @@ -61,7 +60,7 @@ class ThreadBindings extends BaseBindings { Get.lazyPut(() => LoadMoreEmailsInMailboxInteractor(Get.find())); Get.lazyPut(() => SearchEmailInteractor(Get.find())); Get.lazyPut(() => SearchMoreEmailInteractor(Get.find())); - Get.lazyPut(() => GetEmailByIdInteractor(Get.find())); + Get.lazyPut(() => GetEmailByIdInteractor(Get.find(), Get.find())); } @override diff --git a/lib/features/thread/presentation/thread_controller.dart b/lib/features/thread/presentation/thread_controller.dart index be4699f81e..ca96898a56 100644 --- a/lib/features/thread/presentation/thread_controller.dart +++ b/lib/features/thread/presentation/thread_controller.dart @@ -3,12 +3,13 @@ import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/core/state.dart' as jmap; import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; @@ -20,28 +21,36 @@ import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/base_controller.dart'; -import 'package:tmail_ui_user/features/caching/caching_manager.dart'; import 'package:tmail_ui_user/features/composer/domain/state/save_email_as_drafts_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/send_email_state.dart'; import 'package:tmail_ui_user/features/composer/domain/state/update_email_drafts_state.dart'; +import 'package:tmail_ui_user/features/email/domain/model/mark_read_action.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_email_permanently_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/delete_multiple_emails_permanently_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_star_state.dart'; import 'package:tmail_ui_user/features/email/domain/state/move_to_mailbox_state.dart'; import 'package:tmail_ui_user/features/email/presentation/action/email_ui_action.dart'; +import 'package:tmail_ui_user/features/email/presentation/utils/email_utils.dart'; import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/domain/state/remove_email_drafts_state.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/action/dashboard_action.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/search_controller.dart' as search; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/dashboard_routes.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/model/search/search_email_filter.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/model/create_new_email_rule_filter_request.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/state/create_new_rule_filter_state.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/create_new_email_rule_filter_interactor.dart'; +import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' + if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; +import 'package:tmail_ui_user/features/rules_filter_creator/presentation/model/rules_filter_creator_arguments.dart'; import 'package:tmail_ui_user/features/search/email/presentation/search_email_bindings.dart'; import 'package:tmail_ui_user/features/thread/domain/constants/thread_constants.dart'; import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/empty_spam_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/empty_trash_folder_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; import 'package:tmail_ui_user/features/thread/domain/state/get_email_by_id_state.dart'; @@ -61,10 +70,12 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/search_more_email_ import 'package:tmail_ui_user/features/thread/presentation/extensions/list_presentation_email_extensions.dart'; import 'package:tmail_ui_user/features/thread/presentation/mixin/email_action_controller.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; -import 'package:tmail_ui_user/features/thread/presentation/model/search_state.dart'; +import 'package:tmail_ui_user/features/thread/presentation/model/loading_more_status.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/search_status.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/dialog_router.dart'; import 'package:tmail_ui_user/main/routes/navigation_router.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; import 'package:tmail_ui_user/main/routes/route_utils.dart'; @@ -76,22 +87,24 @@ class ThreadController extends BaseController with EmailActionController { final _imagePaths = Get.find(); final _appToast = Get.find(); + final networkConnectionController = Get.find(); final GetEmailsInMailboxInteractor _getEmailsInMailboxInteractor; final RefreshChangesEmailsInMailboxInteractor _refreshChangesEmailsInMailboxInteractor; final LoadMoreEmailsInMailboxInteractor _loadMoreEmailsInMailboxInteractor; final SearchEmailInteractor _searchEmailInteractor; final SearchMoreEmailInteractor _searchMoreEmailInteractor; - final CachingManager _cachingManager; final GetEmailByIdInteractor _getEmailByIdInteractor; + CreateNewEmailRuleFilterInteractor? _createNewEmailRuleFilterInteractor; + final listEmailDrag = [].obs; bool _rangeSelectionMode = false; final openingEmail = RxBool(false); bool canLoadMore = true; bool canSearchMore = true; - bool _isLoadingMore = false; + LoadingMoreStatus loadingMoreStatus = LoadingMoreStatus.idle; MailboxId? _currentMailboxId; jmap.State? _currentEmailState; NavigationRouter? _navigationRouter; @@ -103,13 +116,13 @@ class ThreadController extends BaseController with EmailActionController { ..add(EmailComparator(EmailComparatorProperty.receivedAt) ..setIsAscending(false)); - bool get isLoadingMore => _isLoadingMore; - AccountId? get _accountId => mailboxDashBoardController.accountId.value; + Session? get _session => mailboxDashBoardController.sessionCurrent; + PresentationMailbox? get currentMailbox => mailboxDashBoardController.selectedMailbox.value; - SearchController get searchController => mailboxDashBoardController.searchController; + search.SearchController get searchController => mailboxDashBoardController.searchController; SearchEmailFilter get _searchEmailFilter => searchController.searchEmailFilter.value; @@ -123,7 +136,6 @@ class ThreadController extends BaseController with EmailActionController { this._loadMoreEmailsInMailboxInteractor, this._searchEmailInteractor, this._searchMoreEmailInteractor, - this._cachingManager, this._getEmailByIdInteractor, ); @@ -147,49 +159,68 @@ class ThreadController extends BaseController with EmailActionController { } @override - void onData(Either newState) { - super.onData(newState); - newState.fold( - (failure) { - if (failure is SearchEmailFailure) { - mailboxDashBoardController.emailsInCurrentMailbox.clear(); - } else if (failure is SearchMoreEmailFailure || failure is LoadMoreEmailsFailure) { - _isLoadingMore = false; - } else if (failure is GetEmailByIdFailure) { - openingEmail.value = false; - _navigationRouter = null; - pushAndPop(AppRoutes.unknownRoutePage); - } - }, - (success) { - if (success is GetAllEmailSuccess) { - _getAllEmailSuccess(success); - } else if (success is RefreshChangesAllEmailSuccess) { - _refreshChangesAllEmailSuccess(success); - } else if (success is LoadMoreEmailsSuccess) { - _loadMoreEmailsSuccess(success); - } else if (success is SearchEmailSuccess) { - _searchEmailsSuccess(success); - } else if (success is SearchMoreEmailSuccess) { - _searchMoreEmailsSuccess(success); - } else if (success is SearchingMoreState || success is LoadingMoreState) { - _isLoadingMore = true; - } else if (success is GetEmailByIdLoading) { - openingEmail.value = true; - } else if (success is GetEmailByIdSuccess) { - openingEmail.value = false; - _openEmailDetailView(success.email); - } - } - ); + void handleSuccessViewState(Success success) { + super.handleSuccessViewState(success); + if (success is GetAllEmailSuccess) { + _getAllEmailSuccess(success); + } else if (success is RefreshChangesAllEmailSuccess) { + _refreshChangesAllEmailSuccess(success); + } else if (success is LoadMoreEmailsSuccess) { + _loadMoreEmailsSuccess(success); + } else if (success is SearchEmailSuccess) { + _searchEmailsSuccess(success); + } else if (success is SearchMoreEmailSuccess) { + _searchMoreEmailsSuccess(success); + } else if (success is SearchingMoreState || success is LoadingMoreEmails) { + loadingMoreStatus = LoadingMoreStatus.running; + } else if (success is GetEmailByIdLoading) { + openingEmail.value = true; + } else if (success is GetEmailByIdSuccess) { + openingEmail.value = false; + _openEmailDetailView(success.email); + } else if (success is CreateNewRuleFilterSuccess) { + _createNewRuleFilterSuccess(success); + } } @override - void onDone() {} + void handleFailureViewState(Failure failure) { + super.handleFailureViewState(failure); + if (failure is SearchEmailFailure) { + mailboxDashBoardController.refreshingMailboxState.value = Left(failure); + canSearchMore = false; + mailboxDashBoardController.emailsInCurrentMailbox.clear(); + } else if (failure is SearchMoreEmailFailure || failure is LoadMoreEmailsFailure) { + loadingMoreStatus = LoadingMoreStatus.completed; + } else if (failure is GetEmailByIdFailure) { + openingEmail.value = false; + _navigationRouter = null; + popAndPush(AppRoutes.unknownRoutePage); + } else if (failure is GetAllEmailFailure) { + mailboxDashBoardController.refreshingMailboxState.value = Left(failure); + } + } @override - void onError(error) { - _handleErrorGetAllOrRefreshChangesEmail(error); + void handleErrorViewState(Object error, StackTrace stackTrace) { + super.handleErrorViewState(error, stackTrace); + logError('ThreadController::handleErrorViewState(): error: $error | stackTrace: $stackTrace'); + _resetLoadingMore(); + _handleErrorGetAllOrRefreshChangesEmail(error, stackTrace); + } + + @override + void handleExceptionAction({Failure? failure, Exception? exception}) { + super.handleExceptionAction(failure: failure, exception: exception); + logError('ThreadController::handleExceptionAction(): failure: $failure | exception: $exception'); + _resetLoadingMore(); + clearState(); + } + + void _resetLoadingMore() { + if (loadingMoreStatus == LoadingMoreStatus.running) { + loadingMoreStatus = LoadingMoreStatus.idle; + } } void _registerObxStreamListener() { @@ -198,7 +229,7 @@ class ThreadController extends BaseController with EmailActionController { if (_currentMailboxId != mailbox.id) { _currentMailboxId = mailbox.id; _resetToOriginalValue(); - _getAllEmail(); + _getAllEmailAction(); } } else if (mailbox == null) { // disable current mailbox when search active _currentMailboxId = null; @@ -207,10 +238,8 @@ class ThreadController extends BaseController with EmailActionController { }); ever(searchController.searchState, (searchState) { - if (searchState is SearchState) { - if (searchState.searchStatus == SearchStatus.ACTIVE) { - cancelSelectEmail(); - } + if (searchState.searchStatus == SearchStatus.ACTIVE) { + cancelSelectEmail(); } }); @@ -265,6 +294,12 @@ class ThreadController extends BaseController with EmailActionController { _navigationRouter = action.navigationRouter; _activateSearchFromRouter(); mailboxDashBoardController.clearDashBoardAction(); + } else if (action is SelectDateRangeToAdvancedSearch || action is ClearDateRangeToAdvancedSearch) { + if (listEmailController.hasClients) { + listEmailController.jumpTo(0); + } + canSearchMore = true; + mailboxDashBoardController.emailsInCurrentMailbox.clear(); } }); @@ -278,54 +313,51 @@ class ThreadController extends BaseController with EmailActionController { }); ever(mailboxDashBoardController.viewState, (viewState) { - if (viewState is Either) { - viewState.map((success) { - if (success is MarkAsEmailReadSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MoveToMailboxSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsStarEmailSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is DeleteEmailPermanentlySuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is SaveEmailAsDraftsSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is RemoveEmailDraftsSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is SendEmailSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is UpdateEmailDraftsSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsMailboxReadAllSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsMailboxReadHasSomeEmailFailure) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MoveMultipleEmailToMailboxAllSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is DeleteMultipleEmailsPermanentlyAllSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsStarMultipleEmailAllSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsStarMultipleEmailHasSomeEmailFailure) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsMultipleEmailReadAllSuccess) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is MarkAsMultipleEmailReadHasSomeEmailFailure) { - _refreshEmailChanges(currentEmailState: success.currentEmailState); - } else if (success is EmptyTrashFolderSuccess) { - refreshAllEmail(); - } - }); - } + viewState.map((success) { + if (success is MarkAsEmailReadSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MoveToMailboxSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MarkAsStarEmailSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is DeleteEmailPermanentlySuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is SaveEmailAsDraftsSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is RemoveEmailDraftsSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is SendEmailSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is UpdateEmailDraftsSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MarkAsMailboxReadAllSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MarkAsMailboxReadHasSomeEmailFailure) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MoveMultipleEmailToMailboxAllSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MoveMultipleEmailToMailboxHasSomeEmailFailure) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is DeleteMultipleEmailsPermanentlyAllSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is DeleteMultipleEmailsPermanentlyHasSomeEmailFailure) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MarkAsStarMultipleEmailAllSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MarkAsStarMultipleEmailHasSomeEmailFailure) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MarkAsMultipleEmailReadAllSuccess) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is MarkAsMultipleEmailReadHasSomeEmailFailure) { + _refreshEmailChanges(currentEmailState: success.currentEmailState); + } else if (success is EmptyTrashFolderSuccess || success is EmptySpamFolderSuccess) { + refreshAllEmail(); + } + }); }); } void _activateSearchFromRouter() { - searchController.autoFocus.value = false; searchController.enableSearch(); searchController.updateTextSearch(_navigationRouter!.searchQuery!.value); searchController.updateFilterEmail(text: _navigationRouter!.searchQuery!); @@ -336,19 +368,15 @@ class ThreadController extends BaseController with EmailActionController { _searchEmail(); } - void _handleErrorGetAllOrRefreshChangesEmail(dynamic error) async { + void _handleErrorGetAllOrRefreshChangesEmail(Object error, StackTrace stackTrace) async { logError('ThreadController::_handleErrorGetAllOrRefreshChangesEmail():Error: $error'); if (error is CannotCalculateChangesMethodResponseException) { - await _cachingManager.cleanEmailCache(); - _getAllEmail(); - } else { - super.onError(error); - } - } - - void _getAllEmail() { - if (_accountId != null) { - _getAllEmailAction(_accountId!, mailboxId: _currentMailboxId); + if (_accountId != null && _session != null) { + await cachingManager.clearEmailCacheAndStateCacheByTupleKey(_accountId!, _session!); + } else { + await cachingManager.clearEmailCacheAndAllStateCache(); + } + _getAllEmailAction(); } } @@ -356,11 +384,12 @@ class ThreadController extends BaseController with EmailActionController { dispatchState(Right(LoadingState())); mailboxDashBoardController.emailsInCurrentMailbox.clear(); canLoadMore = true; - _isLoadingMore = false; + loadingMoreStatus = LoadingMoreStatus.idle; cancelSelectEmail(); } void _getAllEmailSuccess(GetAllEmailSuccess success) { + mailboxDashBoardController.refreshingMailboxState.value = Right(success); _currentEmailState = success.currentEmailState; log('ThreadController::_getAllEmailSuccess():_currentEmailState: $_currentEmailState'); final newListEmail = success.emailList.syncPresentationEmail( @@ -394,48 +423,48 @@ class ThreadController extends BaseController with EmailActionController { } } - void _getAllEmailAction(AccountId accountId, {MailboxId? mailboxId}) { - consumeState(_getEmailsInMailboxInteractor.execute( - accountId, - limit: ThreadConstants.defaultLimit, - sort: _sortOrder, - emailFilter: EmailFilter( - filter: _getFilterCondition(), - filterOption: mailboxDashBoardController.filterMessageOption.value, - mailboxId: mailboxId ?? _currentMailboxId), - propertiesCreated: ThreadConstants.propertiesDefault, - propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, - )); + void _getAllEmailAction() { + if (_session != null &&_accountId != null) { + consumeState(_getEmailsInMailboxInteractor.execute( + _session!, + _accountId!, + limit: ThreadConstants.defaultLimit, + sort: _sortOrder, + emailFilter: EmailFilter( + filter: _getFilterCondition(mailboxIdSelected: _currentMailboxId), + filterOption: mailboxDashBoardController.filterMessageOption.value, + mailboxId: _currentMailboxId + ), + propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), + propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, + )); + } } - EmailFilterCondition _getFilterCondition({bool isLoadMore = false}) { - final lastEmail = mailboxDashBoardController.emailsInCurrentMailbox.isNotEmpty - ? mailboxDashBoardController.emailsInCurrentMailbox.last - : null; - final mailboxIdSelected = mailboxDashBoardController.selectedMailbox.value?.id; + EmailFilterCondition _getFilterCondition({PresentationEmail? oldestEmail, MailboxId? mailboxIdSelected}) { switch(mailboxDashBoardController.filterMessageOption.value) { case FilterMessageOption.all: return EmailFilterCondition( inMailbox: mailboxIdSelected, - before: isLoadMore ? lastEmail?.receivedAt : null + before: oldestEmail?.receivedAt ); case FilterMessageOption.unread: return EmailFilterCondition( inMailbox: mailboxIdSelected, notKeyword: KeyWordIdentifier.emailSeen.value, - before: isLoadMore ? lastEmail?.receivedAt : null + before: oldestEmail?.receivedAt ); case FilterMessageOption.attachments: return EmailFilterCondition( inMailbox: mailboxIdSelected, hasAttachment: true, - before: isLoadMore ? lastEmail?.receivedAt : null + before: oldestEmail?.receivedAt ); case FilterMessageOption.starred: return EmailFilterCondition( inMailbox: mailboxIdSelected, hasKeyword: KeyWordIdentifier.emailFlagged.value, - before: isLoadMore ? lastEmail?.receivedAt : null + before: oldestEmail?.receivedAt ); } } @@ -443,13 +472,13 @@ class ThreadController extends BaseController with EmailActionController { void refreshAllEmail() { dispatchState(Right(LoadingState())); canLoadMore = true; + loadingMoreStatus == LoadingMoreStatus.idle; cancelSelectEmail(); if (searchController.isSearchEmailRunning) { - searchController.searchEmailFilter.value = _searchEmailFilter.clearBeforeDate(); _searchEmail(limit: limitEmailFetched); } else { - _getAllEmail(); + _getAllEmailAction(); } } @@ -464,22 +493,23 @@ class ThreadController extends BaseController with EmailActionController { void _refreshEmailChanges({jmap.State? currentEmailState}) { log('ThreadController::_refreshEmailChanges(): currentEmailState: $currentEmailState'); if (searchController.isSearchEmailRunning) { - searchController.searchEmailFilter.value = _searchEmailFilter.clearBeforeDate(); _searchEmail(limit: limitEmailFetched); } else { final newEmailState = currentEmailState ?? _currentEmailState; log('ThreadController::_refreshEmailChanges(): newEmailState: $newEmailState'); - if (_accountId != null && newEmailState != null) { + if (_session != null && _accountId != null && newEmailState != null) { consumeState(_refreshChangesEmailsInMailboxInteractor.execute( - _accountId!, - newEmailState, - sort: _sortOrder, - propertiesCreated: ThreadConstants.propertiesDefault, - propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, - emailFilter: EmailFilter( - filter: _getFilterCondition(), - filterOption: mailboxDashBoardController.filterMessageOption.value, - mailboxId: _currentMailboxId), + _session!, + _accountId!, + newEmailState, + sort: _sortOrder, + propertiesCreated: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), + propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, + emailFilter: EmailFilter( + filter: _getFilterCondition(mailboxIdSelected: _currentMailboxId), + filterOption: mailboxDashBoardController.filterMessageOption.value, + mailboxId: _currentMailboxId + ) )); } } @@ -487,16 +517,21 @@ class ThreadController extends BaseController with EmailActionController { void loadMoreEmails() { log('ThreadController::loadMoreEmails()'); - if (canLoadMore && _accountId != null) { + if (canLoadMore && _session != null && _accountId != null) { + final oldestEmail = mailboxDashBoardController.emailsInCurrentMailbox.isNotEmpty + ? mailboxDashBoardController.emailsInCurrentMailbox.last + : null; consumeState(_loadMoreEmailsInMailboxInteractor.execute( GetEmailRequest( - _accountId!, - limit: ThreadConstants.defaultLimit, - sort: _sortOrder, - filterOption: mailboxDashBoardController.filterMessageOption.value, - filter: _getFilterCondition(isLoadMore: true), - properties: ThreadConstants.propertiesDefault, - lastEmailId: mailboxDashBoardController.emailsInCurrentMailbox.last.id) + _session!, + _accountId!, + limit: ThreadConstants.defaultLimit, + sort: _sortOrder, + filterOption: mailboxDashBoardController.filterMessageOption.value, + filter: _getFilterCondition(oldestEmail: oldestEmail, mailboxIdSelected: _currentMailboxId), + properties: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), + lastEmailId: oldestEmail?.id + ) )); } } @@ -512,6 +547,7 @@ class ThreadController extends BaseController with EmailActionController { } void _loadMoreEmailsSuccess(LoadMoreEmailsSuccess success) { + loadingMoreStatus = LoadingMoreStatus.completed; if (success.emailList.isNotEmpty) { final appendableList = success.emailList .where(_belongToCurrentMailboxId) @@ -528,7 +564,6 @@ class ThreadController extends BaseController with EmailActionController { } else { canLoadMore = false; } - _isLoadingMore = false; } SelectMode getSelectMode(PresentationEmail presentationEmail, PresentationEmail? selectedEmail) { @@ -648,13 +683,14 @@ class ThreadController extends BaseController with EmailActionController { mailboxDashBoardController.filterMessageOption.value = newFilterOption; - _appToast.showToastWithIcon( - currentOverlayContext!, - message: newFilterOption.getMessageToast(context), - icon: newFilterOption.getIconToast(_imagePaths)); + _appToast.showToastMessage( + context, + newFilterOption.getMessageToast(context), + leadingSVGIcon: newFilterOption.getIconToast(_imagePaths), + ); if (searchController.isSearchEmailRunning) { - _searchEmail(filterCondition: _getFilterCondition()); + _searchEmail(); } else { refreshAllEmail(); } @@ -669,28 +705,30 @@ class ThreadController extends BaseController with EmailActionController { searchController.clearTextSearch(); } - void _searchEmail({UnsignedInt? limit, EmailFilterCondition? filterCondition}) { - if (_accountId != null) { - searchController.activateSimpleSearch(); + void _searchEmail({UnsignedInt? limit}) { + if (_session != null && _accountId != null) { + if (listEmailController.hasClients) { + listEmailController.jumpTo(0); + } + mailboxDashBoardController.emailsInCurrentMailbox.clear(); + canSearchMore = true; + searchController.updateFilterEmail(beforeOption: const None()); - filterCondition = EmailFilterCondition( - notKeyword: filterCondition?.notKeyword, - hasKeyword: filterCondition?.hasKeyword, - hasAttachment: filterCondition?.hasAttachment, - ); + searchController.activateSimpleSearch(); consumeState(_searchEmailInteractor.execute( + _session!, _accountId!, limit: limit ?? ThreadConstants.defaultLimit, sort: _sortOrder, - filter: _searchEmailFilter.mappingToEmailFilterCondition(moreFilterCondition: filterCondition), - properties: ThreadConstants.propertiesDefault, + filter: _searchEmailFilter.mappingToEmailFilterCondition(moreFilterCondition: _getFilterCondition()), + properties: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), )); } } void _updateSearchRouteOnBrowser() { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { final route = RouteUtils.generateRouteBrowser( AppRoutes.dashboard, NavigationRouter( @@ -702,6 +740,8 @@ class ThreadController extends BaseController with EmailActionController { } void _searchEmailsSuccess(SearchEmailSuccess success) { + mailboxDashBoardController.refreshingMailboxState.value = Right(success); + canSearchMore = true; final resultEmailSearchList = success.emailList .map((email) => email.toSearchPresentationEmail(mailboxDashBoardController.mapMailboxById)) .toList(); @@ -716,26 +756,31 @@ class ThreadController extends BaseController with EmailActionController { isSearchEmailRunning: searchController.isSearchEmailRunning ); mailboxDashBoardController.updateEmailList(newEmailListSynced); - searchController.autoFocus.value = true; } void searchMoreEmails() { - if (canSearchMore && _accountId != null) { - final lastEmail = mailboxDashBoardController.emailsInCurrentMailbox.last; - searchController.updateFilterEmail(before: lastEmail.receivedAt); + if (canSearchMore && _session != null && _accountId != null) { + final oldestEmail = mailboxDashBoardController.emailsInCurrentMailbox.isNotEmpty + ? mailboxDashBoardController.emailsInCurrentMailbox.last + : null; + searchController.updateFilterEmail(beforeOption: optionOf(oldestEmail?.receivedAt)); consumeState(_searchMoreEmailInteractor.execute( + _session!, _accountId!, limit: ThreadConstants.defaultLimit, sort: _sortOrder, - filter: searchController.searchEmailFilter.value.mappingToEmailFilterCondition(), - properties: ThreadConstants.propertiesDefault, - lastEmailId: lastEmail.id + filter: searchController.searchEmailFilter.value.mappingToEmailFilterCondition(moreFilterCondition: _getFilterCondition()), + properties: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!), + lastEmailId: oldestEmail?.id )); } } void _searchMoreEmailsSuccess(SearchMoreEmailSuccess success) { + loadingMoreStatus = LoadingMoreStatus.completed; + if (success.emailList.isNotEmpty) { + canSearchMore = true; final resultEmailSearchList = success.emailList .map((email) => email.toSearchPresentationEmail(mailboxDashBoardController.mapMailboxById)) .where((email) => !mailboxDashBoardController.emailsInCurrentMailbox.contains(email)) @@ -750,7 +795,6 @@ class ThreadController extends BaseController with EmailActionController { } else { canSearchMore = false; } - _isLoadingMore = false; } bool isSelectionEnabled() => mailboxDashBoardController.isSelectionEnabled(); @@ -831,7 +875,7 @@ class ThreadController extends BaseController with EmailActionController { switch(actionType) { case EmailActionType.preview: if (mailboxContain?.isDrafts == true) { - editEmail(selectedEmail); + editDraftEmail(selectedEmail); } else { previewEmail(selectedEmail); } @@ -840,10 +884,10 @@ class ThreadController extends BaseController with EmailActionController { selectEmail(context, selectedEmail); break; case EmailActionType.markAsRead: - markAsEmailRead(selectedEmail, ReadActions.markAsRead); + markAsEmailRead(selectedEmail, ReadActions.markAsRead, MarkReadAction.tap); break; case EmailActionType.markAsUnread: - markAsEmailRead(selectedEmail, ReadActions.markAsUnread); + markAsEmailRead(selectedEmail, ReadActions.markAsUnread, MarkReadAction.tap); break; case EmailActionType.markAsStarred: markAsStarEmail(selectedEmail, MarkStarAction.markStar); @@ -888,8 +932,8 @@ class ThreadController extends BaseController with EmailActionController { } void calculateDragValue(PresentationEmail? currentPresentationEmail) { - if(currentPresentationEmail != null) { - if(mailboxDashBoardController.listEmailSelected.findEmail(currentPresentationEmail.id) != null){ + if (currentPresentationEmail != null) { + if (currentPresentationEmail.id != null && mailboxDashBoardController.listEmailSelected.findEmail(currentPresentationEmail.id!) != null){ listEmailDrag.clear(); listEmailDrag.addAll(mailboxDashBoardController.listEmailSelected); } else { @@ -914,7 +958,7 @@ class ThreadController extends BaseController with EmailActionController { } PresentationEmail generateEmailByPlatform(PresentationEmail currentEmail) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { final route = RouteUtils.generateRouteBrowser( AppRoutes.dashboard, NavigationRouter( @@ -936,11 +980,13 @@ class ThreadController extends BaseController with EmailActionController { } void _getEmailByIdAction(EmailId emailId) { - if (_accountId != null) { + if (_session != null && _accountId != null) { consumeState(_getEmailByIdInteractor.execute( + _session!, _accountId!, emailId, - properties: ThreadConstants.propertiesDefault)); + properties: EmailUtils.getPropertiesForEmailGetMethod(_session!, _accountId!) + )); } } @@ -969,4 +1015,85 @@ class ThreadController extends BaseController with EmailActionController { void onDragMailBox(bool isDrag) { mailboxDashBoardController.onDragMailbox(isDrag); } + + bool get isNewFolderCreated { + final currentMailbox = mailboxDashBoardController.selectedMailbox.value; + return currentMailbox != null && + currentMailbox.isPersonal && + !currentMailbox.isDefault; + } + + void goToCreateEmailRuleView() async { + final accountId = mailboxDashBoardController.accountId.value; + final session = mailboxDashBoardController.sessionCurrent; + final currentMailbox = mailboxDashBoardController.selectedMailbox.value; + if (accountId != null && session != null) { + final arguments = RulesFilterCreatorArguments( + accountId, + session, + mailboxDestination: currentMailbox + ); + + final newRuleFilterRequest = PlatformInfo.isWeb + ? await DialogRouter.pushGeneralDialog(routeName: AppRoutes.rulesFilterCreator, arguments: arguments) + : await push(AppRoutes.rulesFilterCreator, arguments: arguments); + + if (newRuleFilterRequest is CreateNewEmailRuleFilterRequest) { + _createNewRuleFilterAction(accountId, newRuleFilterRequest); + } + } else { + logError('ThreadController::goToCreateEmailRuleView: Account or Session is NULL'); + } + } + + void _createNewRuleFilterAction( + AccountId accountId, + CreateNewEmailRuleFilterRequest ruleFilterRequest + ) async { + _createNewEmailRuleFilterInteractor = getBinding(); + if (_createNewEmailRuleFilterInteractor != null) { + consumeState(_createNewEmailRuleFilterInteractor!.execute(accountId, ruleFilterRequest)); + } + } + + void _createNewRuleFilterSuccess(CreateNewRuleFilterSuccess success) { + if (success.newListRules.isNotEmpty == true && + currentOverlayContext != null && + currentContext != null) { + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).newFilterWasCreated + ); + } + } + + Future swipeEmailAction(BuildContext context, PresentationEmail email, DismissDirection direction) async { + if (direction == DismissDirection.startToEnd) { + ReadActions readActions = !email.hasRead ? ReadActions.markAsRead : ReadActions.markAsUnread; + markAsEmailRead(email, readActions, MarkReadAction.swipeOnThread); + } + return false; + } + + DismissDirection getSwipeDirection (bool isWebDesktop, SelectMode selectMode) { + if (isWebDesktop) { + return DismissDirection.none; + } + + if (selectMode == SelectMode.ACTIVE) { + return DismissDirection.none; + } + + return DismissDirection.startToEnd; + } + + Future backButtonPressedCallbackAction(BuildContext context) async { + if (PlatformInfo.isMobile && + mailboxDashBoardController.selectedMailbox.value?.isInbox == false) { + mailboxDashBoardController.openDefaultMailbox(); + return false; + } else { + return true; + } + } } \ No newline at end of file diff --git a/lib/features/thread/presentation/thread_view.dart b/lib/features/thread/presentation/thread_view.dart index 596d7878eb..5a2ce26c49 100644 --- a/lib/features/thread/presentation/thread_view.dart +++ b/lib/features/thread/presentation/thread_view.dart @@ -4,8 +4,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:get/get.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:model/model.dart'; import 'package:tmail_ui_user/features/base/mixin/app_loader_mixin.dart'; +import 'package:tmail_ui_user/features/base/widget/compose_floating_button.dart'; import 'package:tmail_ui_user/features/base/mixin/popup_menu_widget_mixin.dart'; import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart'; import 'package:tmail_ui_user/features/email/presentation/widgets/email_action_cupertino_action_sheet_action_builder.dart'; @@ -13,18 +15,25 @@ import 'package:tmail_ui_user/features/mailbox/domain/state/mark_as_mailbox_read import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mixin/filter_email_popup_menu_mixin.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/extensions/vacation_response_extension.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/widgets/vacation_notification_message_widget.dart'; -import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_warning_banner_widget.dart'; +import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_banner_widget.dart'; +import 'package:tmail_ui_user/features/quotas/presentation/widget/quotas_banner_widget.dart'; import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; -import 'package:tmail_ui_user/features/thread/domain/state/search_more_email_state.dart'; import 'package:tmail_ui_user/features/thread/presentation/model/delete_action_type.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/banner_delete_all_spam_emails_styles.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/banner_empty_trash_styles.dart'; import 'package:tmail_ui_user/features/thread/presentation/thread_controller.dart'; -import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar_thread_widget_builder.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/banner_delete_all_spam_emails_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/banner_empty_trash_widget.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/bottom_bar_thread_selection_widget.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_builder.dart' if (dart.library.html) 'package:tmail_ui_user/features/thread/presentation/widgets/email_tile_web_builder.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/empty_emails_widget.dart'; import 'package:tmail_ui_user/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart'; -import 'package:tmail_ui_user/features/thread/presentation/widgets/spam_report_banner_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/spam_banner/spam_report_banner_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/thread_view_bottom_loading_bar_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/thread_view_loading_bar_widget.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -40,54 +49,127 @@ class ThreadView extends GetWidget @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () => FocusManager.instance.primaryFocus?.unfocus(), - child: Scaffold( - resizeToAvoidBottomInset: false, - backgroundColor: Colors.white, - body: Portal( - child: Row(children: [ - if (supportVerticalDivider(context)) - const VerticalDivider( - color: AppColor.colorDividerVertical, - width: 1, - thickness: 0.2), - Expanded(child: SafeArea( - right: _responsiveUtils.isLandscapeMobile(context), - left: _responsiveUtils.isLandscapeMobile(context), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (!_responsiveUtils.isWebDesktop(context)) - ... [ - _buildAppBarNormal(context), - _buildSearchBarView(context), - const SpamReportBannerWidget(), - const QuotasWarningBannerWidget(), - _buildVacationNotificationMessage(context), - ], - _buildEmptyTrashButton(context), - if (!_responsiveUtils.isDesktop(context)) - _buildMarkAsMailboxReadLoading(context), - _buildLoadingView(), - Expanded(child: _buildListEmail(context)), - _buildLoadingViewLoadMore(), - _buildListButtonSelectionForMobile(context), - ] - ) - )) - ]), + return WillPopScope( + onWillPop: () => controller.backButtonPressedCallbackAction(context), + child: GestureDetector( + onTap: () => FocusManager.instance.primaryFocus?.unfocus(), + child: Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.white, + body: Portal( + child: Row(children: [ + if (supportVerticalDivider(context)) + const VerticalDivider(color: AppColor.colorDividerVertical, width: 1), + Expanded(child: SafeArea( + right: _responsiveUtils.isLandscapeMobile(context), + left: _responsiveUtils.isLandscapeMobile(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!_responsiveUtils.isWebDesktop(context)) + ... [ + Obx(() { + return AppBarThreadWidget( + mailboxSelected: controller.currentMailbox, + listEmailSelected: controller.mailboxDashBoardController.emailsInCurrentMailbox.listEmailSelected, + selectMode: controller.mailboxDashBoardController.currentSelectMode.value, + filterOption: controller.mailboxDashBoardController.filterMessageOption.value, + openMailboxAction: controller.openMailboxLeftMenu, + cancelEditThreadAction: controller.cancelSelectEmail, + editThreadAction: controller.enableSelectionEmail, + emailSelectionAction: (actionType, selectionEmail) { + return controller.pressEmailSelectionAction( + context, + actionType, + selectionEmail + ); + }, + onContextMenuFilterEmailAction: _responsiveUtils.isScreenWithShortestSide(context) + ? (filterOption) => controller.openContextMenuAction( + context, + _filterMessagesCupertinoActionTile(context, filterOption) + ) + : null, + onPopupMenuFilterEmailAction: !_responsiveUtils.isScreenWithShortestSide(context) + ? (filterOption, position) => controller.openPopupMenuAction( + context, + position, + popupMenuFilterEmailActionTile( + context, + filterOption, + (option) => controller.filterMessagesAction(context, option) + ) + ) + : null + ); + }), + if (!PlatformInfo.isWeb) + Obx(() { + if (!controller.networkConnectionController.isNetworkConnectionAvailable()) { + return const Padding( + padding: EdgeInsetsDirectional.only(bottom: 8), + child: NetworkConnectionBannerWidget()); + } else { + return const SizedBox.shrink(); + } + }), + _buildSearchBarView(context), + const SpamReportBannerWidget(), + const QuotasBannerWidget(), + _buildVacationNotificationMessage(context), + ], + Obx(() { + if (controller.mailboxDashBoardController.isEmptyTrashBannerEnabledOnMobile(context)) { + return Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: BannerEmptyTrashStyles.mobileMargin + ), + child: BannerEmptyTrashWidget( + onTapAction: () => controller.deleteSelectionEmailsPermanently(context, DeleteActionType.all) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + Obx(() { + if (controller.mailboxDashBoardController.isEmptySpamBannerEnabledOnMobile(context)) { + return Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: BannerDeleteAllSpamEmailsStyles.mobileMargin + ), + child: BannerDeleteAllSpamEmailsWidget( + onTapAction: () => controller.mailboxDashBoardController.openDialogEmptySpamFolder(context) + ), + ); + } else { + return const SizedBox.shrink(); + } + }), + if (!_responsiveUtils.isDesktop(context)) + _buildMarkAsMailboxReadLoading(context), + Obx(() => ThreadViewLoadingBarWidget(viewState: controller.viewState.value)), + Expanded(child: _buildListEmail(context)), + Obx(() => ThreadViewBottomLoadingBarWidget(viewState: controller.viewState.value)), + _buildListButtonSelectionForMobile(context), + ] + ) + )) + ]), + ), + floatingActionButton: _buildFloatingButtonCompose(context), ), - floatingActionButton: _buildFloatingButtonCompose(context), ), ); } bool supportVerticalDivider(BuildContext context) { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return _responsiveUtils.isTabletLarge(context); } else { - return _responsiveUtils.isDesktop(context) || _responsiveUtils.isTabletLarge(context); + return _responsiveUtils.isDesktop(context) || + _responsiveUtils.isTabletLarge(context) || + _responsiveUtils.isLandscapeTablet(context); } } @@ -97,7 +179,7 @@ class ThreadView extends GetWidget padding: EdgeInsets.symmetric( horizontal: 16, vertical: _responsiveUtils.isWebNotDesktop(context) ? 8 : 0), - margin: const EdgeInsets.only(bottom: !BuildUtils.isWeb ? 16 : 0), + margin: EdgeInsets.only(bottom: PlatformInfo.isMobile ? 8 : 0), child: SearchBarView(_imagePaths, hintTextSearch: AppLocalizations.of(context).search_emails, onOpenSearchViewAction: controller.goToSearchView)); @@ -107,13 +189,11 @@ class ThreadView extends GetWidget return Obx(() { final vacation = controller.mailboxDashBoardController.vacationResponse.value; if (vacation?.vacationResponderIsValid == true) { - return Padding( - padding: const EdgeInsets.only(bottom: 8, top: 4), - child: VacationNotificationMessageWidget( - vacationResponse: vacation!, - actionGotoVacationSetting: () => controller.mailboxDashBoardController.goToVacationSetting(), - actionEndNow: () => controller.mailboxDashBoardController.disableVacationResponder()), - ); + return VacationNotificationMessageWidget( + margin: const EdgeInsets.only(bottom: 12, left: 12, right: 12), + vacationResponse: vacation!, + actionGotoVacationSetting: controller.mailboxDashBoardController.goToVacationSetting, + actionEndNow: controller.mailboxDashBoardController.disableVacationResponder); } else { return const SizedBox.shrink(); } @@ -122,59 +202,27 @@ class ThreadView extends GetWidget Widget _buildListButtonSelectionForMobile(BuildContext context) { return Obx(() { - if ((!BuildUtils.isWeb || (BuildUtils.isWeb && controller.isSelectionEnabled() + if ((PlatformInfo.isMobile || (PlatformInfo.isWeb && controller.isSelectionEnabled() && controller.isSearchActive() && !_responsiveUtils.isDesktop(context))) && controller.mailboxDashBoardController.emailsInCurrentMailbox.listEmailSelected.isNotEmpty) { - return Column(children: [ - const Divider(color: AppColor.lineItemListColor, height: 1, thickness: 0.2), - Padding( - padding: const EdgeInsets.all(10), - child: (BottomBarThreadSelectionWidget( - context, - _imagePaths, - _responsiveUtils, - controller.mailboxDashBoardController.emailsInCurrentMailbox.listEmailSelected, - controller.mailboxDashBoardController.selectedMailbox.value) - ..addOnPressEmailSelectionActionClick((actionType, selectionEmail) => - controller.pressEmailSelectionAction(context, actionType, selectionEmail))) - .build()), - ]); + return BottomBarThreadSelectionWidget( + _imagePaths, + _responsiveUtils, + controller.mailboxDashBoardController.emailsInCurrentMailbox.listEmailSelected, + controller.mailboxDashBoardController.selectedMailbox.value, + onPressEmailSelectionActionClick: (actionType, selectionEmail) => + controller.pressEmailSelectionAction( + context, + actionType, + selectionEmail + ) + ); } else { return const SizedBox.shrink(); } }); } - Widget _buildAppBarNormal(BuildContext context) { - return Obx(() { - return AppBarThreadWidgetBuilder( - controller.currentMailbox, - controller.mailboxDashBoardController.emailsInCurrentMailbox.listEmailSelected, - controller.mailboxDashBoardController.currentSelectMode.value, - controller.mailboxDashBoardController.filterMessageOption.value, - onOpenMailboxMenuActionClick: controller.openMailboxLeftMenu, - onCancelEditThread: controller.cancelSelectEmail, - onEditThreadAction: controller.enableSelectionEmail, - onEmailSelectionAction: (actionType, selectionEmail) => - controller.pressEmailSelectionAction(context, actionType, selectionEmail), - onFilterEmailAction: (filterMessageOption, position) { - if (_responsiveUtils.isScreenWithShortestSide(context)) { - controller.openContextMenuAction( - context, - _filterMessagesCupertinoActionTile(context, filterMessageOption)); - } else { - controller.openPopupMenuAction( - context, - position, - popupMenuFilterEmailActionTile( - context, - filterMessageOption, - (option) => controller.filterMessagesAction(context, option))); - } - }); - }); - } - Widget _buildFloatingButtonCompose(BuildContext context) { if (_responsiveUtils.isWebDesktop(context)) { return const SizedBox.shrink(); @@ -183,30 +231,13 @@ class ThreadView extends GetWidget return Obx(() { if (controller.isAllSearchInActive) { return Container( - padding: BuildUtils.isWeb - ? EdgeInsets.zero - : controller.isSelectionEnabled() ? const EdgeInsets.only(bottom: 70) : EdgeInsets.zero, - child: Align( - alignment: Alignment.bottomRight, - child: ScrollingFloatingButtonAnimated( - icon: SvgPicture.asset(_imagePaths.icCompose, width: 20, height: 20, fit: BoxFit.fill), - text: Padding( - padding: const EdgeInsets.only(right: 16), - child: Text(AppLocalizations.of(context).compose, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - style: const TextStyle( - color: AppColor.colorTextButton, - fontSize: 15.0, - fontWeight: FontWeight.w500))), - onPress: () => controller.mailboxDashBoardController.goToComposer(ComposerArguments()), - scrollController: controller.listEmailController, - color: Colors.white, - elevation: 4.0, - width: 140, - animateIcon: false - ) - ) + padding: PlatformInfo.isMobile && controller.listEmailSelected.isNotEmpty + ? EdgeInsets.only(bottom: _responsiveUtils.isTabletLarge(context) ? 85 : 70) + : EdgeInsets.zero, + child: ComposeFloatingButton( + scrollController: controller.listEmailController, + onTap: () => controller.mailboxDashBoardController.goToComposer(ComposerArguments()) + ), ); } else { return const SizedBox.shrink(); @@ -228,8 +259,9 @@ class ThreadView extends GetWidget width: 20, height: 20, fit: BoxFit.fill, - color: filter == FilterMessageOption.attachments - ? AppColor.colorTextButton : null), + colorFilter: filter == FilterMessageOption.attachments + ? AppColor.colorTextButton.asFilter() + : null), filter.getName(context), filter, optionCurrent: optionCurrent, @@ -248,41 +280,9 @@ class ThreadView extends GetWidget .build()).toList(); } - Widget _buildLoadingView() { - return Obx(() => controller.viewState.value.fold( - (failure) => const SizedBox.shrink(), - (success) { - if (controller.isSearchActive() || controller.searchController.advancedSearchIsActivated.isTrue) { - return success is SearchingState - ? Padding(padding: const EdgeInsets.symmetric(vertical: 16), child: loadingWidget) - : const SizedBox.shrink(); - } else { - return success is LoadingState || controller.openingEmail.isTrue - ? Padding(padding: const EdgeInsets.symmetric(vertical: 16), child: loadingWidget) - : const SizedBox.shrink(); - } - })); - } - - Widget _buildLoadingViewLoadMore() { - return Obx(() => controller.viewState.value.fold( - (failure) => const SizedBox.shrink(), - (success) { - if (controller.isSearchActive()) { - return success is SearchingMoreState - ? Padding(padding: const EdgeInsets.only(bottom: 16), child: loadingWidget) - : const SizedBox.shrink(); - } else { - return success is LoadingMoreState - ? Padding(padding: const EdgeInsets.only(bottom: 16), child: loadingWidget) - : const SizedBox.shrink(); - } - })); - } - Widget _buildListEmail(BuildContext context) { return Container( - margin: BuildUtils.isWeb && _responsiveUtils.isDesktop(context) + margin: PlatformInfo.isWeb && _responsiveUtils.isDesktop(context) ? const EdgeInsets.symmetric(horizontal: 4) : EdgeInsets.zero, alignment: Alignment.center, @@ -318,9 +318,10 @@ class ThreadView extends GetWidget return NotificationListener( onNotification: (ScrollNotification scrollInfo) { if (scrollInfo is ScrollEndNotification - && !controller.isLoadingMore + && !controller.loadingMoreStatus.isRunning && scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent ) { + log('ThreadView::_buildListEmailBody(): CALL LOAD MORE'); if (controller.isSearchActive() || controller.searchController.advancedSearchIsActivated.isTrue) { controller.searchMoreEmails(); } else { @@ -340,53 +341,154 @@ class ThreadView extends GetWidget key: const PageStorageKey('list_presentation_email_in_threads'), itemExtent: _getItemExtent(context), itemCount: listPresentationEmail.length, - itemBuilder: (context, index) { - final currentPresentationEmail = listPresentationEmail[index]; - return Obx(() => Draggable>( - maxSimultaneousDrags: kIsWeb ? null : 0, - data: controller.listEmailDrag, - child: (EmailTileBuilder( - context, - currentPresentationEmail, - controller.mailboxDashBoardController.currentSelectMode.value, - controller.searchQuery, - controller.mailboxDashBoardController.selectedEmail.value?.id == currentPresentationEmail.id, - mailboxContain: currentPresentationEmail.mailboxContain, - isSearchEmailRunning: controller.searchController.isSearchEmailRunning) - ..addOnPressEmailActionClick((action, email) => - controller.pressEmailAction(context, action, email, mailboxContain: currentPresentationEmail.mailboxContain)) - ..addOnMoreActionClick((email, position) => _responsiveUtils.isScreenWithShortestSide(context) - ? controller.openContextMenuAction(context, _contextMenuActionTile(context, email)) - : controller.openPopupMenuAction(context, position, _popupMenuActionTile(context, email))) - ).build(), - feedback: _buildFeedBackWidget(context), - childWhenDragging: (EmailTileBuilder( - context, - currentPresentationEmail, - controller.mailboxDashBoardController.currentSelectMode.value, - controller.searchQuery, - controller.mailboxDashBoardController.selectedEmail.value?.id == currentPresentationEmail.id, - mailboxContain: currentPresentationEmail.mailboxContain, - isSearchEmailRunning: controller.searchController.isSearchEmailRunning, - isDrag: true) - ).build(), - dragAnchorStrategy: pointerDragAnchorStrategy, - onDragStarted: () { - controller.calculateDragValue(currentPresentationEmail); - controller.onDragMailBox(true); - }, - onDragEnd: (_) => controller.onDragMailBox(false), - onDraggableCanceled: (_,__) => controller.onDragMailBox(false), - )); - }), + itemBuilder: (context, index) => Obx(() => _buildEmailItem(context, listPresentationEmail[index])) + ), + ) + ); + } + + Widget _buildEmailItem(BuildContext context, PresentationEmail presentationEmail) { + if (_responsiveUtils.isWebDesktop(context)) { + return _buildEmailItemDraggable(context, presentationEmail); + } else { + return _buildEmailItemNotDraggable(context, presentationEmail); + } + } + + Widget _buildEmailItemDraggable(BuildContext context, PresentationEmail presentationEmail) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onSecondaryTapDown: (_) {}, + onTapDown: (_) {}, + child: Draggable>( + data: controller.listEmailDrag, + feedback: _buildFeedBackWidget(context), + childWhenDragging: _buildEmailItemWhenDragging(context, presentationEmail), + dragAnchorStrategy: pointerDragAnchorStrategy, + onDragStarted: () { + controller.calculateDragValue(presentationEmail); + controller.onDragMailBox(true); + }, + onDragEnd: (_) => controller.onDragMailBox(false), + onDraggableCanceled: (_,__) => controller.onDragMailBox(false), + child: _buildEmailItemNotDraggable(context, presentationEmail) + ), + ); + } + + Widget _buildEmailItemWhenDragging(BuildContext context, PresentationEmail presentationEmail) { + final isShowingEmailContent = controller.mailboxDashBoardController.selectedEmail.value?.id == presentationEmail.id; + final selectModeAll = controller.mailboxDashBoardController.currentSelectMode.value; + + return (EmailTileBuilder( + context, + presentationEmail, + selectModeAll, + controller.searchQuery, + isShowingEmailContent, + mailboxContain: presentationEmail.mailboxContain, + isSearchEmailRunning: controller.searchController.isSearchEmailRunning, + isDrag: true + )).build(); + } + + Widget _buildEmailItemNotDraggable(BuildContext context, PresentationEmail presentationEmail) { + final isShowingEmailContent = controller.mailboxDashBoardController.selectedEmail.value?.id == presentationEmail.id; + final selectModeAll = controller.mailboxDashBoardController.currentSelectMode.value; + + return Dismissible( + key: ValueKey(presentationEmail.id), + direction: controller.getSwipeDirection(_responsiveUtils.isWebDesktop(context), selectModeAll), + background: Container( + color: AppColor.colorItemRecipientSelected, + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Row( + children: [ + CircleAvatar( + backgroundColor: AppColor.colorSpamReportBannerBackground, + radius: 24, + child: !presentationEmail.hasRead + ? SvgPicture.asset( + _imagePaths.icMarkAsRead, + fit: BoxFit.fill, + ) + : SvgPicture.asset( + _imagePaths.icUnreadEmail, + fit: BoxFit.fill, + colorFilter: AppColor.primaryColor.asFilter(), + ), + ), + const SizedBox(width: 11), + Text( + !presentationEmail.hasRead + ? AppLocalizations.of(context).mark_as_read + : AppLocalizations.of(context).mark_as_unread, + style: const TextStyle( + fontSize: 15, + color: AppColor.primaryColor, + ), + ), + ], + ), + ), + ), + ), + confirmDismiss: (direction) => controller.swipeEmailAction(context, presentationEmail, direction), + child: (EmailTileBuilder( + context, + presentationEmail, + selectModeAll, + controller.searchQuery, + isShowingEmailContent, + mailboxContain: presentationEmail.mailboxContain, + isSearchEmailRunning: controller.searchController.isSearchEmailRunning ) + ..addOnPressEmailActionClick((action, email) => _handleEmailActionClicked(context, email, action)) + ..addOnMoreActionClick((email, position) => _handleEmailContextMenuAction(context, email, position)) + ).build(), + ); + } + + void _handleEmailActionClicked( + BuildContext context, + PresentationEmail presentationEmail, + EmailActionType actionType + ) { + controller.pressEmailAction( + context, + actionType, + presentationEmail, + mailboxContain: presentationEmail.mailboxContain ); } + void _handleEmailContextMenuAction( + BuildContext context, + PresentationEmail presentationEmail, + RelativeRect? position + ) { + if (_responsiveUtils.isScreenWithShortestSide(context)) { + controller.openContextMenuAction( + context, + _contextMenuActionTile(context, presentationEmail) + ); + } else { + controller.openPopupMenuAction( + context, + position, + _popupMenuActionTile(context, presentationEmail) + ); + } + } + Widget _buildFeedBackWidget(BuildContext context) { return SizedBox( height: 60, child: Material( + clipBehavior: Clip.hardEdge, borderRadius: BorderRadius.circular(10), color: AppColor.colorTextButton, child: Padding( @@ -398,7 +500,7 @@ class ThreadView extends GetWidget width: 24, height: 24, fit: BoxFit.fill, - color: Colors.white, + colorFilter: Colors.white.asFilter(), ), const SizedBox(width: 10), Obx( @@ -420,8 +522,8 @@ class ThreadView extends GetWidget } double? _getItemExtent(BuildContext context) { - if (BuildUtils.isWeb) { - return _responsiveUtils.isDesktop(context) ? 52 : 95; + if (PlatformInfo.isWeb) { + return _responsiveUtils.isDesktop(context) ? 52 : 98; } else { return null; } @@ -431,11 +533,14 @@ class ThreadView extends GetWidget return Obx(() => controller.viewState.value.fold( (failure) => const SizedBox.shrink(), (success) => success is! LoadingState && success is! SearchingState - ? BackgroundWidgetBuilder( - _getMessageEmptyEmail(context), - controller.responsiveUtils, + ? EmptyEmailsWidget( + key: const Key('empty_thread_view'), + title: _getMessageEmptyEmail(context), iconSVG: _imagePaths.icEmptyEmail, subTitle: _getSubMessageEmptyEmail(context), + onCreateFiltersActionCallback: controller.isNewFolderCreated + ? controller.goToCreateEmailRuleView + : null, ) : const SizedBox.shrink()) ); @@ -445,8 +550,9 @@ class ThreadView extends GetWidget if (controller.isSearchActive()) { return AppLocalizations.of(context).no_emails_matching_your_search; } else { - if (controller.mailboxDashBoardController.filterMessageOption.value == FilterMessageOption.all) { - return AppLocalizations.of(context).noEmailInYourCurrentMailbox; + if (controller.mailboxDashBoardController.filterMessageOption.value == FilterMessageOption.all && + controller.isNewFolderCreated) { + return AppLocalizations.of(context).folderCreatedTitle; } else { return AppLocalizations.of(context).noEmailMatchYourCurrentFilter; } @@ -457,72 +563,14 @@ class ThreadView extends GetWidget if (!controller.isSearchActive() && controller.mailboxDashBoardController.filterMessageOption.value != FilterMessageOption.all) { return AppLocalizations.of(context).reduceSomeFiltersAndTryAgain; + } else if (controller.mailboxDashBoardController.filterMessageOption.value == FilterMessageOption.all && + controller.isNewFolderCreated) { + return AppLocalizations.of(context).folderCreatedMessage; } else { return null; } } - bool supportEmptyTrash(BuildContext context) { - return controller.isMailboxTrash - && controller.mailboxDashBoardController.emailsInCurrentMailbox.isNotEmpty - && !controller.isSearchActive() - && !_responsiveUtils.isWebDesktop(context); - } - - Widget _buildEmptyTrashButton(BuildContext context) { - return Obx(() { - if (supportEmptyTrash(context)) { - return Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(14)), - border: Border.all(color: AppColor.colorLineLeftEmailView), - color: Colors.white), - margin: EdgeInsets.only( - left: _responsiveUtils.isWebDesktop(context) ? 0 : 16, - right: 16, - bottom: _responsiveUtils.isWebDesktop(context) ? 0 : 16, - top: _responsiveUtils.isWebDesktop(context) ? 16 : 0), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row(children: [ - Padding( - padding: const EdgeInsets.only(right: 16), - child: SvgPicture.asset( - _imagePaths.icDeleteTrash, - fit: BoxFit.fill)), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(left: 8), - child: Text( - AppLocalizations.of(context).message_delete_all_email_in_trash_button, - style: const TextStyle( - color: AppColor.colorContentEmail, - fontSize: 13, - fontWeight: FontWeight.w500))), - TextButton( - onPressed: () => - controller.deleteSelectionEmailsPermanently(context, DeleteActionType.all), - child: Text( - AppLocalizations.of(context).empty_trash_now, - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.w500, - color: AppColor.colorTextButton) - ) - ) - ] - ) - ) - ]), - ); - } else { - return const SizedBox.shrink(); - } - }); - } - List _contextMenuActionTile(BuildContext context, PresentationEmail email) { final mailboxContain = email.mailboxContain; @@ -545,7 +593,7 @@ class ThreadView extends GetWidget width: 24, height: 24, fit: BoxFit.fill, - color: AppColor.colorTextButton), + colorFilter: AppColor.colorTextButton.asFilter()), mailboxContain?.isSpam == true ? AppLocalizations.of(context).remove_from_spam : AppLocalizations.of(context).mark_as_spam, @@ -572,7 +620,7 @@ class ThreadView extends GetWidget width: 24, height: 24, fit: BoxFit.fill, - color: AppColor.colorTextButton), + colorFilter: AppColor.colorTextButton.asFilter()), AppLocalizations.of(context).openInNewTab, email, iconLeftPadding: _responsiveUtils.isMobile(context) diff --git a/lib/features/thread/presentation/widgets/app_action_sheet_action_builder.dart b/lib/features/thread/presentation/widgets/app_action_sheet_action_builder.dart index fed7751fba..543ccf5665 100644 --- a/lib/features/thread/presentation/widgets/app_action_sheet_action_builder.dart +++ b/lib/features/thread/presentation/widgets/app_action_sheet_action_builder.dart @@ -24,7 +24,7 @@ class AppActionSheetActionBuilder extends CupertinoActionSheetActionBuilder { return Container( color: bgColor ?? Colors.white, child: MouseRegion( - cursor: BuildUtils.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( key: key, child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart b/lib/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart new file mode 100644 index 0000000000..b575d03e3d --- /dev/null +++ b/lib/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart @@ -0,0 +1,76 @@ + +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/mobile_app_bar_thread_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/web_app_bar_thread_widget.dart'; + +typedef OnPopupMenuFilterEmailAction = void Function(FilterMessageOption, RelativeRect); +typedef OnContextMenuFilterEmailAction = void Function(FilterMessageOption); +typedef OnEditThreadAction = void Function(); +typedef OnOpenMailboxMenuActionClick = void Function(); +typedef OnCancelEditThreadAction = void Function(); +typedef OnEmailSelectionAction = void Function(EmailActionType, List); + +class AppBarThreadWidget extends StatelessWidget { + final OnPopupMenuFilterEmailAction? onPopupMenuFilterEmailAction; + final OnContextMenuFilterEmailAction? onContextMenuFilterEmailAction; + final OnOpenMailboxMenuActionClick openMailboxAction; + final OnEditThreadAction editThreadAction; + final OnCancelEditThreadAction cancelEditThreadAction; + final OnEmailSelectionAction emailSelectionAction; + final PresentationMailbox? mailboxSelected; + final List listEmailSelected; + final SelectMode selectMode; + final FilterMessageOption filterOption; + + const AppBarThreadWidget({ + Key? key, + required this.mailboxSelected, + required this.listEmailSelected, + required this.selectMode, + required this.filterOption, + required this.openMailboxAction, + required this.editThreadAction, + required this.cancelEditThreadAction, + required this.emailSelectionAction, + this.onPopupMenuFilterEmailAction, + this.onContextMenuFilterEmailAction, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + if (PlatformInfo.isWeb) { + return WebAppBarThreadWidget( + key: const Key('web_app_bar_thread_widget'), + listEmailSelected: listEmailSelected, + mailboxSelected: mailboxSelected, + selectMode: selectMode, + filterOption: filterOption, + openMailboxAction: openMailboxAction, + editThreadAction: editThreadAction, + cancelEditThreadAction: cancelEditThreadAction, + emailSelectionAction: emailSelectionAction, + onPopupMenuFilterEmailAction: onPopupMenuFilterEmailAction, + onContextMenuFilterEmailAction: onContextMenuFilterEmailAction, + ); + } else { + return MobileAppBarThreadWidget( + key: const Key('mobile_app_bar_thread_widget'), + listEmailSelected: listEmailSelected, + mailboxSelected: mailboxSelected, + selectMode: selectMode, + filterOption: filterOption, + openMailboxAction: openMailboxAction, + editThreadAction: editThreadAction, + cancelEditThreadAction: cancelEditThreadAction, + onPopupMenuFilterEmailAction: onPopupMenuFilterEmailAction, + onContextMenuFilterEmailAction: onContextMenuFilterEmailAction, + ); + } + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/app_bar/default_web_app_bar_thread_widget.dart b/lib/features/thread/presentation/widgets/app_bar/default_web_app_bar_thread_widget.dart new file mode 100644 index 0000000000..a4cf6bcfc2 --- /dev/null +++ b/lib/features/thread/presentation/widgets/app_bar/default_web_app_bar_thread_widget.dart @@ -0,0 +1,77 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/app_bar/default_web_app_bar_thread_widget_style.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class DefaultWebAppBarThreadWidget extends StatelessWidget { + final _imagePaths = Get.find(); + + final PresentationMailbox? mailboxSelected; + final FilterMessageOption filterOption; + final OnOpenMailboxMenuActionClick openMailboxAction; + final OnPopupMenuFilterEmailAction? onPopupMenuFilterEmailAction; + final OnContextMenuFilterEmailAction? onContextMenuFilterEmailAction; + + DefaultWebAppBarThreadWidget({ + super.key, + required this.mailboxSelected, + required this.filterOption, + required this.openMailboxAction, + this.onPopupMenuFilterEmailAction, + this.onContextMenuFilterEmailAction, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + return Container( + color: DefaultWebAppBarThreadWidgetStyle.backgroundColor, + padding: DefaultWebAppBarThreadWidgetStyle.padding, + constraints: const BoxConstraints(minHeight: DefaultWebAppBarThreadWidgetStyle.minHeight), + child: Row( + children: [ + TMailButtonWidget.fromIcon( + key: const Key('mailbox_menu_button'), + icon: _imagePaths.icMenuDrawer, + backgroundColor: Colors.transparent, + padding: DefaultWebAppBarThreadWidgetStyle.mailboxMenuPadding, + maxWidth: DefaultWebAppBarThreadWidgetStyle.buttonMaxWidth, + tooltipMessage: AppLocalizations.of(context).openMailboxMenu, + onTapActionCallback: openMailboxAction, + ), + Expanded( + child: Padding( + padding: DefaultWebAppBarThreadWidgetStyle.titlePadding, + child: Text( + mailboxSelected?.getDisplayName(context) ?? '', + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: DefaultWebAppBarThreadWidgetStyle.titleTextStyle + ), + ), + ), + TMailButtonWidget.fromIcon( + key: const Key('filter_message_button'), + icon: _imagePaths.icFilter, + iconColor: DefaultWebAppBarThreadWidgetStyle.getFilterButtonColor(filterOption), + backgroundColor: Colors.transparent, + maxWidth: DefaultWebAppBarThreadWidgetStyle.buttonMaxWidth, + tooltipMessage: AppLocalizations.of(context).filter_messages, + onTapActionCallback: () => onContextMenuFilterEmailAction?.call(filterOption), + onTapActionAtPositionCallback: (position) => onPopupMenuFilterEmailAction?.call(filterOption, position), + ), + ] + ), + ); + }); + } +} diff --git a/lib/features/thread/presentation/widgets/app_bar/mobile_app_bar_thread_widget.dart b/lib/features/thread/presentation/widgets/app_bar/mobile_app_bar_thread_widget.dart new file mode 100644 index 0000000000..09105d810b --- /dev/null +++ b/lib/features/thread/presentation/widgets/app_bar/mobile_app_bar_thread_widget.dart @@ -0,0 +1,105 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/app_bar/mobile_app_bar_thread_widget_style.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/title_app_bar_thread_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class MobileAppBarThreadWidget extends StatelessWidget { + final _imagePaths = Get.find(); + + final PresentationMailbox? mailboxSelected; + final List listEmailSelected; + final SelectMode selectMode; + final FilterMessageOption filterOption; + final OnOpenMailboxMenuActionClick openMailboxAction; + final OnEditThreadAction editThreadAction; + final OnPopupMenuFilterEmailAction? onPopupMenuFilterEmailAction; + final OnContextMenuFilterEmailAction? onContextMenuFilterEmailAction; + final OnCancelEditThreadAction cancelEditThreadAction; + + MobileAppBarThreadWidget({ + super.key, + required this.listEmailSelected, + required this.mailboxSelected, + required this.selectMode, + required this.filterOption, + required this.openMailboxAction, + required this.editThreadAction, + required this.cancelEditThreadAction, + this.onPopupMenuFilterEmailAction, + this.onContextMenuFilterEmailAction, + }); + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + return Container( + color: MobileAppBarThreadWidgetStyle.backgroundColor, + padding: MobileAppBarThreadWidgetStyle.padding, + constraints: const BoxConstraints(minHeight: MobileAppBarThreadWidgetStyle.minHeight), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + if (selectMode == SelectMode.INACTIVE) + PositionedDirectional( + start: 0, + child: TMailButtonWidget.fromText( + key: const Key('selection_thread_button'), + text: AppLocalizations.of(context).edit, + textStyle: MobileAppBarThreadWidgetStyle.editButtonStyle, + backgroundColor: Colors.transparent, + maxWidth: MobileAppBarThreadWidgetStyle.buttonMaxWidth, + maxLines: 1, + tooltipMessage: AppLocalizations.of(context).edit, + onTapActionCallback: editThreadAction, + ), + ) + else + PositionedDirectional( + start: 0, + child: TMailButtonWidget( + key: const Key('cancel_selection_thread_button'), + text: '${listEmailSelected.length}', + icon: _imagePaths.icBack, + iconColor: MobileAppBarThreadWidgetStyle.backButtonColor, + textStyle: MobileAppBarThreadWidgetStyle.emailCounterTitleStyle, + backgroundColor: Colors.transparent, + onTapActionCallback: cancelEditThreadAction, + ), + ), + Center( + child: TitleAppBarThreadWidget( + key: const Key('title_app_bar_thread'), + mailboxSelected: mailboxSelected, + filterOption: filterOption, + maxWidth: constraints.maxWidth - MobileAppBarThreadWidgetStyle.titleOffset, + openMailboxAction: openMailboxAction, + ), + ), + PositionedDirectional( + end: 0, + child: TMailButtonWidget.fromIcon( + key: const Key('filter_message_button'), + icon: _imagePaths.icFilter, + iconColor: MobileAppBarThreadWidgetStyle.getFilterButtonColor(filterOption), + backgroundColor: Colors.transparent, + maxWidth: MobileAppBarThreadWidgetStyle.buttonMaxWidth, + tooltipMessage: AppLocalizations.of(context).filter_messages, + onTapActionCallback: () => onContextMenuFilterEmailAction?.call(filterOption), + onTapActionAtPositionCallback: (position) => onPopupMenuFilterEmailAction?.call(filterOption, position), + ), + ), + ] + ), + ); + }); + } +} diff --git a/lib/features/thread/presentation/widgets/app_bar/selection_web_app_bar_thread_widget.dart b/lib/features/thread/presentation/widgets/app_bar/selection_web_app_bar_thread_widget.dart new file mode 100644 index 0000000000..0780601ab1 --- /dev/null +++ b/lib/features/thread/presentation/widgets/app_bar/selection_web_app_bar_thread_widget.dart @@ -0,0 +1,156 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/list_presentation_email_extension.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/app_bar/selection_web_app_bar_thread_widget_style.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class SelectionWebAppBarThreadWidget extends StatelessWidget { + final _imagePaths = Get.find(); + + final PresentationMailbox? mailboxSelected; + final List listEmailSelected; + final FilterMessageOption filterOption; + final OnOpenMailboxMenuActionClick openMailboxAction; + final OnCancelEditThreadAction cancelEditThreadAction; + final OnEmailSelectionAction emailSelectionAction; + final OnPopupMenuFilterEmailAction? onPopupMenuFilterEmailAction; + final OnContextMenuFilterEmailAction? onContextMenuFilterEmailAction; + + SelectionWebAppBarThreadWidget({ + super.key, + required this.listEmailSelected, + required this.mailboxSelected, + required this.filterOption, + required this.openMailboxAction, + required this.cancelEditThreadAction, + required this.emailSelectionAction, + this.onPopupMenuFilterEmailAction, + this.onContextMenuFilterEmailAction, + }); + + @override + Widget build(BuildContext context) { + return Container( + color: SelectionWebAppBarThreadWidgetStyle.backgroundColor, + padding: SelectionWebAppBarThreadWidgetStyle.padding, + constraints: const BoxConstraints(minHeight: SelectionWebAppBarThreadWidgetStyle.minHeight), + child: Row( + children: [ + TMailButtonWidget.fromIcon( + key: const Key('cancel_selection_button'), + icon: _imagePaths.icClose, + backgroundColor: Colors.transparent, + iconColor: SelectionWebAppBarThreadWidgetStyle.cancelButtonColor, + iconSize: SelectionWebAppBarThreadWidgetStyle.closeButtonIconSize, + padding: SelectionWebAppBarThreadWidgetStyle.closeButtonPadding, + tooltipMessage: AppLocalizations.of(context).cancel, + onTapActionCallback: cancelEditThreadAction, + ), + Expanded( + child: Text( + AppLocalizations.of(context).count_email_selected(listEmailSelected.length), + maxLines: 1, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: SelectionWebAppBarThreadWidgetStyle.emailCounterStyle + ) + ), + if (mailboxSelected?.isDrafts == false) + TMailButtonWidget.fromIcon( + key: const Key('mark_as_read_email_selection_button'), + icon: listEmailSelected.isAllEmailRead + ? _imagePaths.icUnread + : _imagePaths.icRead, + iconSize: SelectionWebAppBarThreadWidgetStyle.mediumIconSize, + backgroundColor: Colors.transparent, + tooltipMessage: listEmailSelected.isAllEmailRead + ? AppLocalizations.of(context).unread + : AppLocalizations.of(context).read, + onTapActionCallback: () { + return emailSelectionAction( + listEmailSelected.isAllEmailRead + ? EmailActionType.markAsUnread + : EmailActionType.markAsRead, + listEmailSelected + ); + }, + ), + TMailButtonWidget.fromIcon( + key: const Key('mark_as_star_email_selection_button'), + icon: listEmailSelected.isAllEmailStarred + ? _imagePaths.icUnStar + : _imagePaths.icStar, + iconSize: SelectionWebAppBarThreadWidgetStyle.mediumIconSize, + backgroundColor: Colors.transparent, + tooltipMessage: listEmailSelected.isAllEmailStarred + ? AppLocalizations.of(context).not_starred + : AppLocalizations.of(context).starred, + onTapActionCallback: () { + return emailSelectionAction( + listEmailSelected.isAllEmailStarred + ? EmailActionType.unMarkAsStarred + : EmailActionType.markAsStarred, + listEmailSelected + ); + }, + ), + if (mailboxSelected?.isDrafts == false) + TMailButtonWidget.fromIcon( + key: const Key('move_email_selection_button'), + icon: _imagePaths.icMove, + iconSize: SelectionWebAppBarThreadWidgetStyle.iconSize, + backgroundColor: Colors.transparent, + tooltipMessage: AppLocalizations.of(context).move_message, + onTapActionCallback: () => emailSelectionAction(EmailActionType.moveToMailbox, listEmailSelected), + ), + if (mailboxSelected?.isDrafts == false) + TMailButtonWidget.fromIcon( + key: const Key('mark_as_spam_email_selection_button'), + icon: mailboxSelected?.isSpam == true + ? _imagePaths.icNotSpam + : _imagePaths.icSpam, + iconSize: SelectionWebAppBarThreadWidgetStyle.iconSize, + backgroundColor: Colors.transparent, + tooltipMessage: mailboxSelected?.isSpam == true + ? AppLocalizations.of(context).un_spam + : AppLocalizations.of(context).mark_as_spam, + onTapActionCallback: () { + return mailboxSelected?.isSpam == true + ? emailSelectionAction(EmailActionType.unSpam, listEmailSelected) + : emailSelectionAction(EmailActionType.moveToSpam, listEmailSelected); + }, + ), + TMailButtonWidget.fromIcon( + key: const Key('delete_email_selection_button'), + icon: _deletePermanentlyValid + ? _imagePaths.icDeleteComposer + : _imagePaths.icDelete, + iconSize: SelectionWebAppBarThreadWidgetStyle.iconSize, + iconColor: SelectionWebAppBarThreadWidgetStyle.getDeleteButtonColor(_deletePermanentlyValid), + backgroundColor: Colors.transparent, + tooltipMessage: _deletePermanentlyValid + ? AppLocalizations.of(context).delete_permanently + : AppLocalizations.of(context).move_to_trash, + onTapActionCallback: () { + return _deletePermanentlyValid + ? emailSelectionAction(EmailActionType.deletePermanently, listEmailSelected) + : emailSelectionAction(EmailActionType.moveToTrash, listEmailSelected); + }, + ), + ] + ), + ); + } + + bool get _deletePermanentlyValid => mailboxSelected?.isTrash == true || mailboxSelected?.isDrafts == true; +} diff --git a/lib/features/thread/presentation/widgets/app_bar/title_app_bar_thread_widget.dart b/lib/features/thread/presentation/widgets/app_bar/title_app_bar_thread_widget.dart new file mode 100644 index 0000000000..d4b3db8512 --- /dev/null +++ b/lib/features/thread/presentation/widgets/app_bar/title_app_bar_thread_widget.dart @@ -0,0 +1,75 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:tmail_ui_user/features/mailbox/presentation/extensions/presentation_mailbox_extension.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/app_bar/title_app_bar_thread_widget_style.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart'; + +class TitleAppBarThreadWidget extends StatelessWidget { + final _imagePaths = Get.find(); + + final PresentationMailbox? mailboxSelected; + final FilterMessageOption filterOption; + final OnOpenMailboxMenuActionClick openMailboxAction; + final double maxWidth; + + TitleAppBarThreadWidget({ + super.key, + required this.mailboxSelected, + required this.filterOption, + required this.maxWidth, + required this.openMailboxAction, + }); + + @override + Widget build(BuildContext context) { + if (filterOption == FilterMessageOption.all) { + if (mailboxSelected != null) { + return TMailButtonWidget( + text: mailboxSelected!.getDisplayName(context), + icon: _imagePaths.icChevronDown, + textStyle: TitleAppBarThreadWidgetStyle.titleStyle, + backgroundColor: Colors.transparent, + iconAlignment: TextDirection.rtl, + mainAxisSize: MainAxisSize.min, + padding: TitleAppBarThreadWidgetStyle.padding, + maxWidth: maxWidth, + flexibleText: true, + maxLines: 1, + tooltipMessage: mailboxSelected!.getDisplayName(context), + onTapActionCallback: openMailboxAction, + ); + } else { + return const SizedBox.shrink(); + } + } else { + return Column( + children: [ + if (mailboxSelected != null) + TMailButtonWidget( + text: mailboxSelected!.getDisplayName(context), + icon: _imagePaths.icChevronDown, + textStyle: TitleAppBarThreadWidgetStyle.titleStyle, + backgroundColor: Colors.transparent, + iconAlignment: TextDirection.rtl, + mainAxisSize: MainAxisSize.min, + padding: TitleAppBarThreadWidgetStyle.padding, + maxWidth: maxWidth, + flexibleText: true, + maxLines: 1, + tooltipMessage: mailboxSelected!.getDisplayName(context), + onTapActionCallback: openMailboxAction, + ), + Text( + filterOption.getTitle(context), + style: TitleAppBarThreadWidgetStyle.filterOptionStyle + ) + ], + ); + } + } +} diff --git a/lib/features/thread/presentation/widgets/app_bar/web_app_bar_thread_widget.dart b/lib/features/thread/presentation/widgets/app_bar/web_app_bar_thread_widget.dart new file mode 100644 index 0000000000..36148d275d --- /dev/null +++ b/lib/features/thread/presentation/widgets/app_bar/web_app_bar_thread_widget.dart @@ -0,0 +1,63 @@ + +import 'package:flutter/cupertino.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:model/mailbox/select_mode.dart'; +import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/app_bar_thread_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/default_web_app_bar_thread_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/app_bar/selection_web_app_bar_thread_widget.dart'; + +class WebAppBarThreadWidget extends StatelessWidget { + + final PresentationMailbox? mailboxSelected; + final List listEmailSelected; + final SelectMode selectMode; + final FilterMessageOption filterOption; + final OnOpenMailboxMenuActionClick openMailboxAction; + final OnEditThreadAction editThreadAction; + final OnCancelEditThreadAction cancelEditThreadAction; + final OnEmailSelectionAction emailSelectionAction; + final OnPopupMenuFilterEmailAction? onPopupMenuFilterEmailAction; + final OnContextMenuFilterEmailAction? onContextMenuFilterEmailAction; + + const WebAppBarThreadWidget({ + super.key, + required this.listEmailSelected, + required this.mailboxSelected, + required this.selectMode, + required this.filterOption, + required this.openMailboxAction, + required this.editThreadAction, + required this.cancelEditThreadAction, + required this.emailSelectionAction, + this.onPopupMenuFilterEmailAction, + this.onContextMenuFilterEmailAction, + }); + + @override + Widget build(BuildContext context) { + if (selectMode == SelectMode.INACTIVE) { + return DefaultWebAppBarThreadWidget( + key: const Key('default_web_app_bar_thread_widget'), + mailboxSelected: mailboxSelected, + filterOption: filterOption, + openMailboxAction: openMailboxAction, + onPopupMenuFilterEmailAction: onPopupMenuFilterEmailAction, + onContextMenuFilterEmailAction: onContextMenuFilterEmailAction, + ); + } else { + return SelectionWebAppBarThreadWidget( + key: const Key('selection_web_app_bar_thread_widget'), + listEmailSelected: listEmailSelected, + mailboxSelected: mailboxSelected, + filterOption: filterOption, + openMailboxAction: openMailboxAction, + cancelEditThreadAction: cancelEditThreadAction, + emailSelectionAction: emailSelectionAction, + onPopupMenuFilterEmailAction: onPopupMenuFilterEmailAction, + onContextMenuFilterEmailAction: onContextMenuFilterEmailAction, + ); + } + } +} diff --git a/lib/features/thread/presentation/widgets/app_bar_thread_widget_builder.dart b/lib/features/thread/presentation/widgets/app_bar_thread_widget_builder.dart deleted file mode 100644 index 112adbaa6f..0000000000 --- a/lib/features/thread/presentation/widgets/app_bar_thread_widget_builder.dart +++ /dev/null @@ -1,406 +0,0 @@ - -import 'package:core/core.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:model/model.dart'; -import 'package:tmail_ui_user/features/thread/domain/model/filter_message_option.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -typedef OnFilterEmailAction = void Function(FilterMessageOption, RelativeRect? position); -typedef OnEditThreadAction = void Function(); -typedef OnOpenMailboxMenuActionClick = void Function(); -typedef OnCancelEditThread = void Function(); -typedef OnEmailSelectionAction = void Function(EmailActionType actionType, List); - -class AppBarThreadWidgetBuilder extends StatelessWidget { - final _imagePaths = Get.find(); - final _responsiveUtils = Get.find(); - - final OnFilterEmailAction? onFilterEmailAction; - final OnOpenMailboxMenuActionClick? onOpenMailboxMenuActionClick; - final OnEditThreadAction? onEditThreadAction; - final OnCancelEditThread? onCancelEditThread; - final OnEmailSelectionAction? onEmailSelectionAction; - - final PresentationMailbox? _currentMailbox; - final List _listSelectionEmail; - final SelectMode _selectMode; - final FilterMessageOption _filterMessageOption; - - AppBarThreadWidgetBuilder( - this._currentMailbox, - this._listSelectionEmail, - this._selectMode, - this._filterMessageOption, { - Key? key, - this.onFilterEmailAction, - this.onOpenMailboxMenuActionClick, - this.onEditThreadAction, - this.onCancelEditThread, - this.onEmailSelectionAction, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return LayoutBuilder(builder: (context, constraints) { - return Container( - key: const Key('app_bar_thread_widget'), - alignment: Alignment.center, - height: 56, - color: Colors.white, - padding: const EdgeInsets.symmetric(horizontal: 16), - child: _buildAppBar(context, constraints) - ); - }); - } - - Widget _buildAppBar(BuildContext context, BoxConstraints constraints) { - if (BuildUtils.isWeb) { - return _selectMode == SelectMode.INACTIVE - ? _buildBodyAppBarForWeb(context) - : _buildBodyAppBarForWebSelection(context); - } else { - return _selectMode == SelectMode.INACTIVE - ? _buildBodyAppBarForMobile(context, constraints) - : _buildBodyAppBarForMobileSelection(context, constraints); - } - } - - Widget _buildBodyAppBarForWeb(BuildContext context) { - return Row(children: [ - if (_responsiveUtils.hasLeftMenuDrawerActive(context)) - Padding( - padding: const EdgeInsets.only(right: 16), - child: _buildMenuButton(), - ), - Expanded(child: Text( - _currentMailbox?.name?.name.capitalizeFirstEach ?? '', - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - style: const TextStyle( - fontSize: 21, - color: Colors.black, - fontWeight: FontWeight.bold))), - _buildFilterButton(context), - ]); - } - - Widget _buildBodyAppBarForWebSelection(BuildContext context) { - return Row(children: [ - buildIconWeb( - icon: SvgPicture.asset(_imagePaths.icCloseComposer, - color: AppColor.colorTextButton, - fit: BoxFit.fill), - minSize: 25, - iconSize: 25, - iconPadding: const EdgeInsets.all(5), - splashRadius: 15, - tooltip: AppLocalizations.of(context).cancel, - onTap: () => onCancelEditThread?.call()), - Expanded(child: Text( - AppLocalizations.of(context).count_email_selected(_listSelectionEmail.length), - style: const TextStyle( - fontSize: 17, - fontWeight: FontWeight.w500, - color: AppColor.colorTextButton))), - if(_currentMailbox?.isDrafts == false) - buildIconWeb( - minSize: 25, - iconSize: 25, - iconPadding: const EdgeInsets.all(5), - splashRadius: 15, - icon: SvgPicture.asset( - _listSelectionEmail.isAllEmailRead - ? _imagePaths.icUnread - : _imagePaths.icRead, - fit: BoxFit.fill), - tooltip: _listSelectionEmail.isAllEmailRead - ? AppLocalizations.of(context).unread - : AppLocalizations.of(context).read, - onTap: () => onEmailSelectionAction?.call( - _listSelectionEmail.isAllEmailRead - ? EmailActionType.markAsUnread - : EmailActionType.markAsRead, - _listSelectionEmail)), - const SizedBox(width: 5), - buildIconWeb( - minSize: 25, - iconSize: 25, - iconPadding: const EdgeInsets.all(5), - splashRadius: 15, - icon: SvgPicture.asset( - _listSelectionEmail.isAllEmailStarred - ? _imagePaths.icUnStar - : _imagePaths.icStar, - fit: BoxFit.fill), - tooltip: _listSelectionEmail.isAllEmailStarred - ? AppLocalizations.of(context).not_starred - : AppLocalizations.of(context).starred, - onTap: () => onEmailSelectionAction?.call( - _listSelectionEmail.isAllEmailStarred - ? EmailActionType.unMarkAsStarred - : EmailActionType.markAsStarred, - _listSelectionEmail)), - const SizedBox(width: 5), - if (_currentMailbox?.isDrafts == false) - ... [ - buildIconWeb( - minSize: 25, - iconSize: 25, - iconPadding: const EdgeInsets.all(5), - splashRadius: 15, - icon: SvgPicture.asset(_imagePaths.icMove, fit: BoxFit.fill), - tooltip: AppLocalizations.of(context).move, - onTap: () => onEmailSelectionAction?.call(EmailActionType.moveToMailbox, _listSelectionEmail)), - const SizedBox(width: 5), - buildIconWeb( - minSize: 25, - iconSize: 25, - iconPadding: const EdgeInsets.all(5), - splashRadius: 15, - icon: SvgPicture.asset(_currentMailbox?.isSpam == true - ? _imagePaths.icNotSpam : _imagePaths.icSpam, - fit: BoxFit.fill), - tooltip: _currentMailbox?.isSpam == true - ? AppLocalizations.of(context).un_spam - : AppLocalizations.of(context).mark_as_spam, - onTap: () => _currentMailbox?.isSpam == true - ? onEmailSelectionAction?.call(EmailActionType.unSpam, _listSelectionEmail) - : onEmailSelectionAction?.call(EmailActionType.moveToSpam, _listSelectionEmail)), - const SizedBox(width: 5), - ], - buildIconWeb( - minSize: 25, - iconSize: 25, - iconPadding: const EdgeInsets.all(5), - splashRadius: 15, - icon: SvgPicture.asset( - canDeletePermanently ? _imagePaths.icDeleteComposer : _imagePaths.icDelete, - color: canDeletePermanently ? AppColor.colorDeletePermanentlyButton : AppColor.primaryColor, - width: 20, - height: 20, - fit: BoxFit.fill), - tooltip: canDeletePermanently - ? AppLocalizations.of(context).delete_permanently - : AppLocalizations.of(context).move_to_trash, - onTap: () => canDeletePermanently - ? onEmailSelectionAction?.call(EmailActionType.deletePermanently, _listSelectionEmail) - : onEmailSelectionAction?.call(EmailActionType.moveToTrash, _listSelectionEmail)), - const SizedBox(width: 10), - ]); - } - - bool get canDeletePermanently { - return _currentMailbox?.isTrash == true || _currentMailbox?.isDrafts == true; - } - - Widget _buildBodyAppBarForMobile(BuildContext context, BoxConstraints constraints) { - return Stack( - alignment: Alignment.center, - children: [ - if (_filterMessageOption.getTitle(context).isNotEmpty) - Center( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildContentCenterAppBar(context, constraints), - Transform( - transform: Matrix4.translationValues( - _getXTranslationValues(context), - -8.0, - 0.0), - child: Text( - _filterMessageOption.getTitle(context), - style: const TextStyle(fontSize: 11, color: AppColor.colorContentEmail))) - ]), - ) - else - Center(child: _buildContentCenterAppBar(context, constraints)), - Positioned(left: 0, child: _buildEditButton(context)), - Positioned(right: 0, child: _buildFilterButton(context)) - ] - ); - } - - Widget _buildBodyAppBarForMobileSelection(BuildContext context, BoxConstraints constraints) { - return Stack( - alignment: Alignment.center, - children: [ - if (_filterMessageOption.getTitle(context).isNotEmpty) - Center( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildContentCenterAppBar(context, constraints), - Transform( - transform: Matrix4.translationValues( - _responsiveUtils.isDesktop(context) ? -2.0 : -16.0, - -8.0, - 0.0), - child: Text( - _filterMessageOption.getTitle(context), - style: const TextStyle( - fontSize: 11, - color: AppColor.colorContentEmail))) - ]), - ) - else - Center(child: _buildContentCenterAppBar(context, constraints)), - Positioned(left: 0, child: _buildCancelSelection()), - Positioned(right: 0, child: _buildFilterButton(context)) - ] - ); - } - - Widget _buildEditButton(BuildContext context) { - return Material( - borderRadius: BorderRadius.circular(15), - color: Colors.transparent, - child: TextButton( - onPressed: onEditThreadAction, - child: Text( - AppLocalizations.of(context).edit, - style: const TextStyle( - fontSize: 17, - color: AppColor.colorTextButton), - ), - ) - ); - } - - Widget _buildFilterButton(BuildContext context) { - return Material( - borderRadius: BorderRadius.circular(15), - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(15), - onTap: () { - if (onFilterEmailAction != null - && _responsiveUtils.isScreenWithShortestSide(context)) { - onFilterEmailAction!.call(_filterMessageOption, null); - } - }, - child: Container( - color: Colors.transparent, - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), - child: SvgPicture.asset( - _imagePaths.icFilter, - color: _filterMessageOption == FilterMessageOption.all - ? AppColor.colorFilterMessageDisabled - : AppColor.colorFilterMessageEnabled, - fit: BoxFit.fill), - ), - onTapDown: (detail) { - if (onFilterEmailAction != null - && !_responsiveUtils.isScreenWithShortestSide(context)) { - final screenSize = MediaQuery.of(context).size; - final offset = detail.globalPosition; - final position = RelativeRect.fromLTRB( - offset.dx, - offset.dy, - screenSize.width - offset.dx, - screenSize.height - offset.dy, - ); - onFilterEmailAction!.call(_filterMessageOption, position); - } - }) - ); - } - - Widget _buildMenuButton() { - return buildIconWeb( - minSize: 20, - iconSize: 20, - iconPadding: const EdgeInsets.all(3), - splashRadius: 15, - icon: SvgPicture.asset(_imagePaths.icMenuDrawer, fit: BoxFit.fill), - onTap: onOpenMailboxMenuActionClick); - } - - Widget _buildCancelSelection() { - return Material( - borderRadius: BorderRadius.circular(15), - color: Colors.transparent, - child: InkWell( - onTap: onCancelEditThread, - borderRadius: BorderRadius.circular(15), - child: Container( - color: Colors.transparent, - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SvgPicture.asset(_imagePaths.icBack, - width: 20, - height: 20, - color: AppColor.colorTextButton, - fit: BoxFit.fill), - const SizedBox(width: 8), - _buildCountItemSelected() - ]), - )), - ); - } - - Widget _buildCountItemSelected() { - return Padding( - padding: EdgeInsets.zero, - child: Text( - '${_listSelectionEmail.length}', - maxLines: 1, - overflow: CommonTextStyle.defaultTextOverFlow, - softWrap: CommonTextStyle.defaultSoftWrap, - style: const TextStyle( - fontSize: 17, - color: AppColor.colorTextButton))); - } - - Widget _buildContentCenterAppBar(BuildContext context, BoxConstraints constraints) { - if (_responsiveUtils.hasLeftMenuDrawerActive(context)) { - return Material( - color: Colors.transparent, - child: InkWell( - onTap: onOpenMailboxMenuActionClick, - borderRadius: BorderRadius.circular(15), - child: Container( - color: Colors.transparent, - height: 40, - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - constraints: BoxConstraints(maxWidth: constraints.maxWidth - 220), - child: Text( - _currentMailbox?.name?.name.capitalizeFirstEach ?? '', - maxLines: 1, - softWrap: CommonTextStyle.defaultSoftWrap, - overflow: CommonTextStyle.defaultTextOverFlow, - style: const TextStyle( - fontSize: 21, - color: AppColor.colorNameEmail, - fontWeight: FontWeight.w700))), - SvgPicture.asset(_imagePaths.icChevronDown) - ] - ), - ), - ), - ); - } else { - return const SizedBox.shrink(); - } - } - - double _getXTranslationValues(BuildContext context) { - if (BuildUtils.isWeb) { - return _responsiveUtils.isWebDesktop(context) ? -2.0 : -16.0; - } else { - return _responsiveUtils.isTabletLarge(context) ? 0.0 : -16.0; - } - } -} \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/banner_delete_all_spam_emails_widget.dart b/lib/features/thread/presentation/widgets/banner_delete_all_spam_emails_widget.dart new file mode 100644 index 0000000000..19e4d90741 --- /dev/null +++ b/lib/features/thread/presentation/widgets/banner_delete_all_spam_emails_widget.dart @@ -0,0 +1,79 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/banner_delete_all_spam_emails_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class BannerDeleteAllSpamEmailsWidget extends StatelessWidget { + + final VoidCallback onTapAction; + + const BannerDeleteAllSpamEmailsWidget({super.key, required this.onTapAction}); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + return Container( + decoration: const ShapeDecoration( + color: BannerDeleteAllSpamEmailsStyles.backgroundColor, + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1, + color: BannerDeleteAllSpamEmailsStyles.borderStrokeColor, + ), + borderRadius: BorderRadius.all(Radius.circular(BannerDeleteAllSpamEmailsStyles.borderRadius)), + ), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTapAction, + borderRadius: const BorderRadius.all(Radius.circular(BannerDeleteAllSpamEmailsStyles.borderRadius)), + child: Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: BannerDeleteAllSpamEmailsStyles.horizontalPadding, + vertical: BannerDeleteAllSpamEmailsStyles.verticalPadding + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + imagePaths.icDeleteRuleMobile, + width: BannerDeleteAllSpamEmailsStyles.iconSize, + height: BannerDeleteAllSpamEmailsStyles.iconSize, + fit: BoxFit.fill + ), + const SizedBox(width: BannerDeleteAllSpamEmailsStyles.space), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context).deleteAllSpamEmailsNow, + style: const TextStyle( + fontSize: BannerDeleteAllSpamEmailsStyles.buttonTextSize, + fontWeight: FontWeight.w500, + color: BannerDeleteAllSpamEmailsStyles.buttonTextColor + ) + ), + Text( + AppLocalizations.of(context).bannerDeleteAllSpamEmailsMessage, + style: const TextStyle( + color: BannerDeleteAllSpamEmailsStyles.labelTextColor, + fontSize: BannerDeleteAllSpamEmailsStyles.labelTextSize, + fontWeight: FontWeight.w400 + ) + ), + ] + ) + ) + ] + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/thread/presentation/widgets/banner_empty_trash_widget.dart b/lib/features/thread/presentation/widgets/banner_empty_trash_widget.dart new file mode 100644 index 0000000000..c98d9e5960 --- /dev/null +++ b/lib/features/thread/presentation/widgets/banner_empty_trash_widget.dart @@ -0,0 +1,79 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/banner_empty_trash_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class BannerEmptyTrashWidget extends StatelessWidget { + + final VoidCallback onTapAction; + + const BannerEmptyTrashWidget({super.key, required this.onTapAction}); + + @override + Widget build(BuildContext context) { + final imagePaths = Get.find(); + return Container( + decoration: const ShapeDecoration( + color: BannerEmptyTrashStyles.backgroundColor, + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1, + color: BannerEmptyTrashStyles.borderStrokeColor, + ), + borderRadius: BorderRadius.all(Radius.circular(BannerEmptyTrashStyles.borderRadius)), + ), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTapAction, + borderRadius: const BorderRadius.all(Radius.circular(BannerEmptyTrashStyles.borderRadius)), + child: Padding( + padding: const EdgeInsetsDirectional.symmetric( + horizontal: BannerEmptyTrashStyles.horizontalPadding, + vertical: BannerEmptyTrashStyles.verticalPadding + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SvgPicture.asset( + imagePaths.icDeleteRuleMobile, + width: BannerEmptyTrashStyles.iconSize, + height: BannerEmptyTrashStyles.iconSize, + fit: BoxFit.fill + ), + const SizedBox(width: BannerEmptyTrashStyles.space), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + AppLocalizations.of(context).empty_trash_now, + style: const TextStyle( + fontSize: BannerEmptyTrashStyles.buttonTextSize, + fontWeight: FontWeight.w500, + color: BannerEmptyTrashStyles.buttonTextColor + ) + ), + Text( + AppLocalizations.of(context).message_delete_all_email_in_trash_button, + style: const TextStyle( + color: BannerEmptyTrashStyles.labelTextColor, + fontSize: BannerEmptyTrashStyles.labelTextSize, + fontWeight: FontWeight.w400 + ) + ), + ] + ) + ) + ] + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/thread/presentation/widgets/bottom_bar_thread_selection_widget.dart b/lib/features/thread/presentation/widgets/bottom_bar_thread_selection_widget.dart index 55388ba464..c1b8a1f067 100644 --- a/lib/features/thread/presentation/widgets/bottom_bar_thread_selection_widget.dart +++ b/lib/features/thread/presentation/widgets/bottom_bar_thread_selection_widget.dart @@ -1,114 +1,165 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; import 'package:flutter/material.dart'; -import 'package:model/model.dart'; +import 'package:model/email/email_action_type.dart'; +import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/list_presentation_email_extension.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; typedef OnPressEmailSelectionActionClick = void Function(EmailActionType, List); -class BottomBarThreadSelectionWidget { +class BottomBarThreadSelectionWidget extends StatelessWidget{ - final BuildContext _context; final ImagePaths _imagePaths; final ResponsiveUtils _responsiveUtils; final List _listSelectionEmail; final PresentationMailbox? _currentMailbox; + final OnPressEmailSelectionActionClick? onPressEmailSelectionActionClick; - OnPressEmailSelectionActionClick? _onPressEmailSelectionActionClick; - - BottomBarThreadSelectionWidget( - this._context, + const BottomBarThreadSelectionWidget( this._imagePaths, this._responsiveUtils, this._listSelectionEmail, this._currentMailbox, + { + super.key, + this.onPressEmailSelectionActionClick, + } ); - void addOnPressEmailSelectionActionClick(OnPressEmailSelectionActionClick onPressEmailSelectionActionClick) { - _onPressEmailSelectionActionClick = onPressEmailSelectionActionClick; - } - - Widget build() { + @override + Widget build(BuildContext context) { return Container( - key: const Key('bottom_bar_thread_selection_widget'), - alignment: Alignment.center, - color: Colors.white, - child: MediaQuery( - data: const MediaQueryData(padding: EdgeInsets.zero), - child: _buildListOptionButton() - ) - ); - } - - Widget _buildListOptionButton() { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if(_currentMailbox?.isDrafts == false) - Expanded(child: (ButtonBuilder(_listSelectionEmail.isAllEmailRead ? _imagePaths.icUnread : _imagePaths.icRead) - ..key(const Key('button_mark_read_email')) - ..paddingIcon(const EdgeInsets.symmetric(horizontal: 8, vertical: 4)) - ..textStyle(const TextStyle(fontSize: 12, color: AppColor.colorTextButton)) - ..onPressActionClick(() { - if (_onPressEmailSelectionActionClick != null) { - _onPressEmailSelectionActionClick!( + decoration: const BoxDecoration( + border: Border(top: BorderSide( + color: AppColor.colorDividerHorizontal, + width: 0.5, + )), + ), + child: IntrinsicHeight( + child: Row( + children: [ + if (_currentMailbox?.isDrafts == false) + Expanded( + child: TMailButtonWidget( + key: const Key('mark_as_read_selected_email_button'), + text: _listSelectionEmail.isAllEmailRead + ? AppLocalizations.of(context).unread + : AppLocalizations.of(context).read, + icon: _listSelectionEmail.isAllEmailRead ? _imagePaths.icUnread : _imagePaths.icRead, + borderRadius: 0, + iconSize: 20, + flexibleText: true, + textAlign: TextAlign.center, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + backgroundColor: Colors.transparent, + textStyle: const TextStyle(fontSize: 12, color: AppColor.colorTextButton), + verticalDirection: _verticalDirection(context), + onTapActionCallback: () { + onPressEmailSelectionActionClick?.call( _listSelectionEmail.isAllEmailRead ? EmailActionType.markAsUnread : EmailActionType.markAsRead, - _listSelectionEmail); - }}) - ..text(_textButtonMarkAsRead, isVertical: _responsiveUtils.isMobile(_context))) - .build()), - Expanded(child: (ButtonBuilder(_listSelectionEmail.isAllEmailStarred ? _imagePaths.icUnStar : _imagePaths.icStar) - ..key(const Key('button_mark_as_star_email')) - ..paddingIcon(const EdgeInsets.symmetric(horizontal: 8, vertical: 4)) - ..textStyle(const TextStyle(fontSize: 12, color: AppColor.colorTextButton)) - ..onPressActionClick(() { - if (_onPressEmailSelectionActionClick != null) { - _onPressEmailSelectionActionClick!( + _listSelectionEmail + ); + }, + ), + ), + Expanded( + child: TMailButtonWidget( + key: const Key('mark_as_star_selected_email_button'), + text: _listSelectionEmail.isAllEmailStarred + ? AppLocalizations.of(context).un_star + : AppLocalizations.of(context).star, + icon: _listSelectionEmail.isAllEmailStarred ? _imagePaths.icUnStar : _imagePaths.icStar, + borderRadius: 0, + iconSize: 20, + flexibleText: true, + textAlign: TextAlign.center, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + backgroundColor: Colors.transparent, + textStyle: const TextStyle(fontSize: 12, color: AppColor.colorTextButton), + verticalDirection: _verticalDirection(context), + onTapActionCallback: () { + onPressEmailSelectionActionClick?.call( _listSelectionEmail.isAllEmailStarred ? EmailActionType.unMarkAsStarred : EmailActionType.markAsStarred, - _listSelectionEmail); - }}) - ..text(_textButtonMarkAsStar, isVertical: _responsiveUtils.isMobile(_context))) - .build()), - if (_currentMailbox?.isDrafts == false) - Expanded(child: (ButtonBuilder(_imagePaths.icMove) - ..key(const Key('button_move_to_mailbox')) - ..paddingIcon(const EdgeInsets.symmetric(horizontal: 8, vertical: 4)) - ..textStyle(const TextStyle(fontSize: 12, color: AppColor.colorTextButton)) - ..onPressActionClick(() { - if (_onPressEmailSelectionActionClick != null) { - _onPressEmailSelectionActionClick!(EmailActionType.moveToMailbox, _listSelectionEmail); - }}) - ..text(_textButtonMove, isVertical: _responsiveUtils.isMobile(_context))) - .build()), - if (_currentMailbox?.isDrafts == false) - Expanded(child: (ButtonBuilder(_currentMailbox?.isSpam == true ? _imagePaths.icNotSpam : _imagePaths.icSpam) - ..key(const Key('button_move_to_spam')) - ..paddingIcon(const EdgeInsets.symmetric(horizontal: 8, vertical: 4)) - ..textStyle(const TextStyle(fontSize: 12, color: AppColor.colorTextButton)) - ..onPressActionClick(() { - if (_currentMailbox?.isSpam == true) { - _onPressEmailSelectionActionClick?.call(EmailActionType.unSpam, _listSelectionEmail); - } else { - _onPressEmailSelectionActionClick?.call(EmailActionType.moveToSpam, _listSelectionEmail); - } - }) - ..text(_textButtonSpam, isVertical: _responsiveUtils.isMobile(_context))) - .build()), - Expanded(child: (ButtonBuilder(canDeletePermanently ? _imagePaths.icDeleteComposer : _imagePaths.icDelete) - ..key(const Key('button_delete_email')) - ..iconColor(canDeletePermanently ? AppColor.colorDeletePermanentlyButton : AppColor.primaryColor) - ..paddingIcon(const EdgeInsets.symmetric(horizontal: 8, vertical: 4)) - ..textStyle(const TextStyle(fontSize: 12, color: AppColor.colorTextButton)) - ..onPressActionClick(() { - if (canDeletePermanently) { - _onPressEmailSelectionActionClick?.call(EmailActionType.deletePermanently, _listSelectionEmail); - } else { - _onPressEmailSelectionActionClick?.call(EmailActionType.moveToTrash, _listSelectionEmail); - } - }) - ..text(_textButtonDelete, isVertical: _responsiveUtils.isMobile(_context))) - .build()) - ] + _listSelectionEmail + ); + }, + ), + ), + if (_currentMailbox?.isDrafts == false) + Expanded( + child: TMailButtonWidget( + key: const Key('move_selected_email_to_mailbox_button'), + text: AppLocalizations.of(context).move, + icon: _imagePaths.icMove, + borderRadius: 0, + iconSize: 20, + textAlign: TextAlign.center, + flexibleText: true, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + backgroundColor: Colors.transparent, + textStyle: const TextStyle(fontSize: 12, color: AppColor.colorTextButton), + verticalDirection: _verticalDirection(context), + onTapActionCallback: () { + onPressEmailSelectionActionClick?.call(EmailActionType.moveToMailbox, _listSelectionEmail); + }, + ), + ), + if (_currentMailbox?.isDrafts == false) + Expanded( + child: TMailButtonWidget( + key: const Key('move_selected_email_to_spam_button'), + text: _currentMailbox?.isSpam == true + ? AppLocalizations.of(context).un_spam + : AppLocalizations.of(context).spam, + icon: _currentMailbox?.isSpam == true ? _imagePaths.icNotSpam : _imagePaths.icSpam, + borderRadius: 0, + iconSize: 20, + flexibleText: true, + textAlign: TextAlign.center, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + backgroundColor: Colors.transparent, + textStyle: const TextStyle(fontSize: 12, color: AppColor.colorTextButton), + verticalDirection: _verticalDirection(context), + onTapActionCallback: () { + if (_currentMailbox?.isSpam == true) { + onPressEmailSelectionActionClick?.call(EmailActionType.unSpam, _listSelectionEmail); + } else { + onPressEmailSelectionActionClick?.call(EmailActionType.moveToSpam, _listSelectionEmail); + } + }, + ), + ), + Expanded( + child: TMailButtonWidget( + key: const Key('delete_selected_email_button'), + text: AppLocalizations.of(context).delete, + icon: canDeletePermanently ? _imagePaths.icDeleteComposer : _imagePaths.icDelete, + iconColor: canDeletePermanently ? AppColor.colorDeletePermanentlyButton : AppColor.primaryColor, + borderRadius: 0, + iconSize: 20, + flexibleText: true, + textAlign: TextAlign.center, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12), + backgroundColor: Colors.transparent, + textStyle: const TextStyle(fontSize: 12, color: AppColor.colorTextButton), + verticalDirection: _verticalDirection(context), + onTapActionCallback: () { + if (canDeletePermanently) { + onPressEmailSelectionActionClick?.call(EmailActionType.deletePermanently, _listSelectionEmail); + } else { + onPressEmailSelectionActionClick?.call(EmailActionType.moveToTrash, _listSelectionEmail); + } + }, + ), + ), + ] + ), + ) ); } @@ -116,54 +167,9 @@ class BottomBarThreadSelectionWidget { return _currentMailbox?.isTrash == true || _currentMailbox?.isDrafts == true; } - String? get _textButtonMarkAsRead { - if (!_isMailboxDashboardSplitView(_context)) { - return _listSelectionEmail.isAllEmailRead - ? AppLocalizations.of(_context).unread - : AppLocalizations.of(_context).read; - } - return null; - } - - String? get _textButtonMarkAsStar { - if (!_isMailboxDashboardSplitView(_context)) { - return _listSelectionEmail.isAllEmailStarred - ? AppLocalizations.of(_context).un_star - : AppLocalizations.of(_context).star; - } - return null; - } - - String? get _textButtonMove { - if (!_isMailboxDashboardSplitView(_context)) { - return AppLocalizations.of(_context).move; - } - return null; - } - - String? get _textButtonSpam { - if (!_isMailboxDashboardSplitView(_context)) { - return _currentMailbox?.isSpam == true - ? AppLocalizations.of(_context).un_spam - : AppLocalizations.of(_context).spam; - } - return null; - } - - String? get _textButtonDelete { - if (!_isMailboxDashboardSplitView(_context)) { - return AppLocalizations.of(_context).delete; - } - return null; - } - - bool _isMailboxDashboardSplitView(BuildContext context) { - if (BuildUtils.isWeb) { - return _responsiveUtils.isTabletLarge(context); - } else { - return _responsiveUtils.isLandscapeTablet(context) || - _responsiveUtils.isTabletLarge(context) || - _responsiveUtils.isDesktop(context); - } + bool _verticalDirection(BuildContext context) { + return _responsiveUtils.isLandscapeMobile(context) || + _responsiveUtils.isPortraitMobile(context) || + _responsiveUtils.isTabletLarge(context); } } \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/email_tile_builder.dart b/lib/features/thread/presentation/widgets/email_tile_builder.dart index 71159dd30a..d507267d61 100644 --- a/lib/features/thread/presentation/widgets/email_tile_builder.dart +++ b/lib/features/thread/presentation/widgets/email_tile_builder.dart @@ -16,8 +16,8 @@ class EmailTileBuilder with BaseEmailItemTile { final PresentationMailbox? mailboxContain; final SearchQuery? _searchQuery; final bool isSearchEmailRunning; - final EdgeInsets? padding; - final EdgeInsets? paddingDivider; + final EdgeInsetsGeometry? padding; + final EdgeInsetsGeometry? paddingDivider; final bool isDrag; final bool _isShowingEmailContent; @@ -51,7 +51,7 @@ class EmailTileBuilder with BaseEmailItemTile { children: [ ListTile( tileColor: _isShowingEmailContent ? AppColor.colorItemEmailSelectedDesktop : null, - contentPadding: padding ?? const EdgeInsets.symmetric(horizontal: 16, vertical: 5), + contentPadding: padding ?? const EdgeInsetsDirectional.symmetric(horizontal: 16, vertical: 5), onTap: () => _emailActionClick?.call( EmailActionType.preview, _presentationEmail), @@ -75,23 +75,25 @@ class EmailTileBuilder with BaseEmailItemTile { children: [ if (!_presentationEmail.hasRead) Padding( - padding: const EdgeInsets.only(right: 5), + padding: const EdgeInsetsDirectional.only(end: 5), child: SvgPicture.asset( imagePaths.icUnreadStatus, width: 9, height: 9, fit: BoxFit.fill)), Expanded(child: buildInformationSender( + _context, _presentationEmail, mailboxContain, isSearchEmailRunning, _searchQuery)), + buildIconAnsweredOrForwarded(width: 16, height: 16, presentationEmail: _presentationEmail), if (_presentationEmail.hasAttachment == true) Padding( - padding: const EdgeInsets.only(left: 8), + padding: const EdgeInsetsDirectional.only(start: 8), child: buildIconAttachment()), Padding( - padding: const EdgeInsets.only(right: 4, left: 8), + padding: const EdgeInsetsDirectional.only(end: 4, start: 8), child: buildDateTime(_context, _presentationEmail)), buildIconChevron(), ], @@ -101,28 +103,33 @@ class EmailTileBuilder with BaseEmailItemTile { mainAxisSize: MainAxisSize.min, children: [ Padding( - padding: const EdgeInsets.only(top: 6), + padding: const EdgeInsetsDirectional.only(top: 6), child: Row( mainAxisSize: MainAxisSize.min, children: [ + if (_presentationEmail.hasCalendarEvent) + buildCalendarEventIcon(context: _context, presentationEmail: _presentationEmail), Expanded(child: buildEmailTitle( + _context, _presentationEmail, isSearchEmailRunning, _searchQuery)), buildMailboxContain( + _context, isSearchEmailRunning, _presentationEmail), if (_presentationEmail.hasStarred) Padding( - padding: const EdgeInsets.only(left: 8), + padding: const EdgeInsetsDirectional.only(start: 8), child: buildIconStar(), ) ], )), Padding( - padding: const EdgeInsets.only(top: 6), + padding: const EdgeInsetsDirectional.only(top: 6), child: Row(children: [ Expanded(child: buildEmailPartialContent( + _context, _presentationEmail, isSearchEmailRunning, _searchQuery)), @@ -132,11 +139,8 @@ class EmailTileBuilder with BaseEmailItemTile { ), ), Padding( - padding: paddingDivider ?? const EdgeInsets.symmetric(horizontal: 16), - child: const Divider( - color: AppColor.lineItemListColor, - height: 1, - thickness: 0.2)), + padding: paddingDivider ?? const EdgeInsetsDirectional.symmetric(horizontal: 16), + child: const Divider(color: AppColor.lineItemListColor, height: 1)), ], ), ); diff --git a/lib/features/thread/presentation/widgets/email_tile_web_builder.dart b/lib/features/thread/presentation/widgets/email_tile_web_builder.dart index c8d551f9a6..ea7c6ef4e9 100644 --- a/lib/features/thread/presentation/widgets/email_tile_web_builder.dart +++ b/lib/features/thread/presentation/widgets/email_tile_web_builder.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:model/email/email_action_type.dart'; import 'package:model/email/presentation_email.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; @@ -86,19 +87,19 @@ class EmailTileBuilder with BaseEmailItemTile { ); } - EdgeInsets _getMarginItem() { + EdgeInsetsGeometry _getMarginItem() { if (responsiveUtils.isDesktop(_context)) { return const EdgeInsets.only(top: 3); } else { - return const EdgeInsets.only(top: 3, left: 16, right: 16); + return const EdgeInsetsDirectional.only(top: 3, start: 16, end: 16); } } - EdgeInsets _getPaddingItem() { + EdgeInsetsGeometry _getPaddingItem() { if (responsiveUtils.isDesktop(_context)) { return const EdgeInsets.symmetric(vertical: 8); } else { - return const EdgeInsets.only(bottom: 8, right: 8, top: 8); + return const EdgeInsetsDirectional.only(bottom: 8, end: 8, top: 8); } } @@ -132,7 +133,7 @@ class EmailTileBuilder with BaseEmailItemTile { : EmailActionType.preview, _presentationEmail), child: Padding( - padding: const EdgeInsets.only(top: 8, right: 12), + padding: const EdgeInsetsDirectional.only(top: 8, end: 12), child: _buildAvatarIcon(), ), ), @@ -141,28 +142,30 @@ class EmailTileBuilder with BaseEmailItemTile { Row(children: [ if (!_presentationEmail.hasRead) Padding( - padding: const EdgeInsets.only(right: 5), + padding: const EdgeInsetsDirectional.only(end: 5), child: SvgPicture.asset( imagePaths.icUnreadStatus, width: 9, height: 9, fit: BoxFit.fill)), Expanded(child: buildInformationSender( + _context, _presentationEmail, mailboxContain, isSearchEmailRunning, _searchQuery )), + buildIconAnsweredOrForwarded(width: 16, height: 16, presentationEmail: _presentationEmail), if (_presentationEmail.hasAttachment == true) Padding( - padding: const EdgeInsets.only(left: 8), + padding: const EdgeInsetsDirectional.only(start: 8), child: SvgPicture.asset( imagePaths.icAttachment, width: 16, height: 16, fit: BoxFit.fill)), Padding( - padding: const EdgeInsets.only(right: 4, left: 8), + padding: const EdgeInsetsDirectional.only(end: 4, start: 8), child: buildDateTime(_context, _presentationEmail)), buildIconChevron() ]), @@ -170,18 +173,22 @@ class EmailTileBuilder with BaseEmailItemTile { Row( mainAxisSize: MainAxisSize.min, children: [ + if (_presentationEmail.hasCalendarEvent) + buildCalendarEventIcon(context: _context, presentationEmail: _presentationEmail), Expanded(child: buildEmailTitle( + _context, _presentationEmail, isSearchEmailRunning, _searchQuery )), buildMailboxContain( + _context, isSearchEmailRunning, _presentationEmail ), if (_presentationEmail.hasStarred) Padding( - padding: const EdgeInsets.only(left: 8), + padding: const EdgeInsetsDirectional.only(start: 8), child: buildIconStar(), ), ], @@ -189,6 +196,7 @@ class EmailTileBuilder with BaseEmailItemTile { const SizedBox(height: 8), Row(children: [ Expanded(child: buildEmailPartialContent( + _context, _presentationEmail, isSearchEmailRunning, _searchQuery @@ -200,11 +208,8 @@ class EmailTileBuilder with BaseEmailItemTile { ), if (_selectModeAll == SelectMode.INACTIVE) Transform( - transform: Matrix4.translationValues(0.0, 10.0, 0.0), - child: const Divider( - color: AppColor.lineItemListColor, - height: 1, - thickness: 0.2)), + transform: Matrix4.translationValues(0.0, 10.0, 0.0), + child: const Divider(color: AppColor.lineItemListColor, height: 1)), ], ); } @@ -220,16 +225,10 @@ class EmailTileBuilder with BaseEmailItemTile { children: [ Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ InkWell( - onTap: () { - if (isHoverIcon) { - _emailActionClick?.call( - EmailActionType.selection, - _presentationEmail); - } - }, + onTap: () => _emailActionClick?.call(EmailActionType.selection, _presentationEmail), onHover: (value) => _onHoverIconChanged(value, setState), child: Padding( - padding: const EdgeInsets.only(top: 8, right: 12), + padding: const EdgeInsetsDirectional.only(top: 8, end: 12), child: _buildAvatarIcon())), Expanded(child: Stack(alignment: Alignment.bottomCenter, children: [ @@ -237,13 +236,14 @@ class EmailTileBuilder with BaseEmailItemTile { Row(children: [ if (!_presentationEmail.hasRead) Padding( - padding: const EdgeInsets.only(right: 5), - child: SvgPicture.asset( - imagePaths.icUnreadStatus, - width: 9, - height: 9, - fit: BoxFit.fill)), + padding: const EdgeInsetsDirectional.only(end: 5), + child: SvgPicture.asset( + imagePaths.icUnreadStatus, + width: 9, + height: 9, + fit: BoxFit.fill)), Expanded(child: buildInformationSender( + _context, _presentationEmail, mailboxContain, isSearchEmailRunning, @@ -258,18 +258,22 @@ class EmailTileBuilder with BaseEmailItemTile { Row( mainAxisSize: MainAxisSize.min, children: [ + if (_presentationEmail.hasCalendarEvent) + buildCalendarEventIcon(context: _context, presentationEmail: _presentationEmail), Expanded(child: buildEmailTitle( + _context, _presentationEmail, isSearchEmailRunning, _searchQuery )), buildMailboxContain( + _context, isSearchEmailRunning, _presentationEmail ), if (_presentationEmail.hasStarred) Padding( - padding: const EdgeInsets.only(left: 8), + padding: const EdgeInsetsDirectional.only(start: 8), child: buildIconStar(), ) ], @@ -277,6 +281,7 @@ class EmailTileBuilder with BaseEmailItemTile { const SizedBox(height: 8), Row(children: [ Expanded(child: buildEmailPartialContent( + _context, _presentationEmail, isSearchEmailRunning, _searchQuery @@ -286,10 +291,7 @@ class EmailTileBuilder with BaseEmailItemTile { if (_selectModeAll == SelectMode.INACTIVE) Transform( transform: Matrix4.translationValues(0.0, 10.0, 0.0), - child: const Divider( - color: AppColor.lineItemListColor, - height: 1, - thickness: 0.2), + child: const Divider(color: AppColor.lineItemListColor, height: 1), ), ], )) @@ -320,7 +322,7 @@ class EmailTileBuilder with BaseEmailItemTile { child: Stack(alignment: Alignment.bottomCenter, children: [ Row(children: [ Container( - padding: const EdgeInsets.only(left: 16, right: 16), + padding: const EdgeInsetsDirectional.symmetric(horizontal: 16), alignment: Alignment.center, child: !_presentationEmail.hasRead ? SvgPicture.asset( @@ -345,26 +347,28 @@ class EmailTileBuilder with BaseEmailItemTile { ? EmailActionType.unMarkAsStarred : EmailActionType.markAsStarred, _presentationEmail)), + buildIconWeb( + icon: buildIconAnsweredOrForwarded(presentationEmail: _presentationEmail), + tooltip: messageToolTipForAnsweredOrForwarded(context, _presentationEmail), + iconPadding: const EdgeInsetsDirectional.only(end: 12), + splashRadius: 1), InkWell( - onTap: () { - if (isHoverIcon) { - _emailActionClick?.call( - EmailActionType.selection, - _presentationEmail); - } - }, + onTap: () => _emailActionClick?.call(EmailActionType.selection, _presentationEmail), onHover: (value) => _onHoverIconChanged(value, setState), child: _buildAvatarIcon( - iconSize: 32, - textStyle: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.white)), + iconSize: 32, + textStyle: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white + ) + ), ), const SizedBox(width: 10), SizedBox( width: 160, child: buildInformationSender( + _context, _presentationEmail, mailboxContain, isSearchEmailRunning, @@ -382,11 +386,8 @@ class EmailTileBuilder with BaseEmailItemTile { Transform( transform: Matrix4.translationValues(0.0, 10, 0.0), child: const Padding( - padding: EdgeInsets.only(left: 80), - child: Divider( - color: AppColor.lineItemListColor, - height: 1, - thickness: 0.2)), + padding: EdgeInsetsDirectional.only(start: 120), + child: Divider(color: AppColor.lineItemListColor, height: 1)), ) ]), ); @@ -482,28 +483,30 @@ class EmailTileBuilder with BaseEmailItemTile { Widget _buildDateTimeForDesktopScreen() { return Row(children: [ buildMailboxContain( + _context, isSearchEmailRunning, _presentationEmail ), if (_presentationEmail.hasAttachment == true) Padding( - padding: const EdgeInsets.only(left: 8), - child: buildIconAttachment()), + padding: const EdgeInsetsDirectional.only(start: 8), + child: buildIconAttachment()), Padding( - padding: const EdgeInsets.only(right: 20, left: 8), - child: buildDateTime(_context, _presentationEmail)) + padding: const EdgeInsetsDirectional.only(end: 20, start: 8), + child: buildDateTime(_context, _presentationEmail)) ]); } Widget _buildDateTimeForMobileTabletScreen() { return Row(children: [ + buildIconAnsweredOrForwarded(width: 16, height: 16, presentationEmail: _presentationEmail), if (_presentationEmail.hasAttachment == true) Padding( - padding: const EdgeInsets.only(left: 8), - child: buildIconAttachment()), + padding: const EdgeInsetsDirectional.only(start: 8), + child: buildIconAttachment()), Padding( - padding: const EdgeInsets.only(right: 4, left: 8), - child: buildDateTime(_context, _presentationEmail)), + padding: const EdgeInsetsDirectional.only(end: 4, start: 8), + child: buildDateTime(_context, _presentationEmail)), buildIconChevron() ]); } @@ -511,17 +514,21 @@ class EmailTileBuilder with BaseEmailItemTile { Widget _buildSubjectAndContent() { return LayoutBuilder(builder: (context, constraints) { return Row(children: [ + if (_presentationEmail.hasCalendarEvent) + buildCalendarEventIcon(context: _context, presentationEmail: _presentationEmail), if (_presentationEmail.getEmailTitle().isNotEmpty) Container( constraints: BoxConstraints(maxWidth: constraints.maxWidth / 2), - padding: const EdgeInsets.only(right: 12), + padding: const EdgeInsetsDirectional.only(end: 12), child: buildEmailTitle( + _context, _presentationEmail, isSearchEmailRunning, _searchQuery )), Expanded(child: Container( child: buildEmailPartialContent( + _context, _presentationEmail, isSearchEmailRunning, _searchQuery @@ -531,19 +538,27 @@ class EmailTileBuilder with BaseEmailItemTile { }); } + bool get _isSelectionActivated { + return isHoverIcon || + isHoverItem || + _presentationEmail.selectMode == SelectMode.ACTIVE || + (responsiveUtils.isMobile(_context) && _selectModeAll == SelectMode.ACTIVE); + } + Widget _buildAvatarIcon({double? iconSize, TextStyle? textStyle}) { - if (isHoverIcon || _presentationEmail.selectMode == SelectMode.ACTIVE || - (responsiveUtils.isMobile(_context) && _selectModeAll == SelectMode.ACTIVE)) { - return buildIconAvatarSelection( - _context, - _presentationEmail, - iconSize: iconSize ?? 48, - textStyle: textStyle); + if (_isSelectionActivated) { + return buildIconAvatarSelection( + _context, + _presentationEmail, + iconSize: iconSize ?? 48, + textStyle: textStyle + ); } else { return buildIconAvatarText( - _presentationEmail, - iconSize: iconSize ?? 48, - textStyle: textStyle); + _presentationEmail, + iconSize: iconSize ?? 48, + textStyle: textStyle + ); } } } \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/empty_emails_widget.dart b/lib/features/thread/presentation/widgets/empty_emails_widget.dart new file mode 100644 index 0000000000..d229c89825 --- /dev/null +++ b/lib/features/thread/presentation/widgets/empty_emails_widget.dart @@ -0,0 +1,113 @@ + +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/presentation/views/button/tmail_button_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/empty_emails_widget_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +typedef OnCreateFiltersActionCallback = Function(); + +class EmptyEmailsWidget extends StatelessWidget { + + final String title; + final String? iconSVG; + final String? subTitle; + final OnCreateFiltersActionCallback? onCreateFiltersActionCallback; + final Color? titleColor; + + const EmptyEmailsWidget({ + Key? key, + required this.title, + this.iconSVG, + this.subTitle, + this.onCreateFiltersActionCallback, + this.titleColor, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final responsiveUtils = Get.find(); + final childWidget = Padding( + padding: EmptyEmailsWidgetStyles.padding, + child: Column( + mainAxisAlignment: responsiveUtils.isScreenWithShortestSide(context) + ? MainAxisAlignment.start + : MainAxisAlignment.center, + children: [ + if (iconSVG != null) + SvgPicture.asset( + iconSVG!, + width: _getIconSize(context, responsiveUtils), + height: _getIconSize(context, responsiveUtils), + fit: BoxFit.fill + ), + Padding( + padding: EmptyEmailsWidgetStyles.labelPadding, + child: Text( + title, + style: TextStyle( + color: EmptyEmailsWidgetStyles.labelTextColor, + fontSize: onCreateFiltersActionCallback != null + ? EmptyEmailsWidgetStyles.createFilterLabelTextSize + : EmptyEmailsWidgetStyles.labelTextSize, + fontWeight: onCreateFiltersActionCallback != null + ? EmptyEmailsWidgetStyles.createFilterLabelFontWeight + : EmptyEmailsWidgetStyles.labelFontWeight + ), + textAlign: TextAlign.center, + ), + ), + if (subTitle != null) + Text( + subTitle!, + style: const TextStyle( + color: EmptyEmailsWidgetStyles.messageTextColor, + fontSize: EmptyEmailsWidgetStyles.messageTextSize, + fontWeight: EmptyEmailsWidgetStyles.messageFontWeight + ), + textAlign: TextAlign.center, + ), + if (onCreateFiltersActionCallback != null) + TMailButtonWidget.fromText( + text: AppLocalizations.of(context).createFilters, + padding: EmptyEmailsWidgetStyles.createFilterButtonPadding, + margin: EmptyEmailsWidgetStyles.createFilterButtonMargin, + backgroundColor: EmptyEmailsWidgetStyles.createFilterButtonBackgroundColor, + borderRadius: EmptyEmailsWidgetStyles.createFilterButtonBorderRadius, + width: responsiveUtils.isPortraitMobile(context) ? double.infinity : null, + textAlign: TextAlign.center, + textStyle: const TextStyle( + fontSize: EmptyEmailsWidgetStyles.createFilterButtonTextSize, + fontWeight: EmptyEmailsWidgetStyles.createFilterButtonFontWeight, + color: EmptyEmailsWidgetStyles.createFilterButtonTextColor + ), + onTapActionCallback: onCreateFiltersActionCallback, + ) + ], + ), + ); + return Container( + constraints: const BoxConstraints(maxWidth: EmptyEmailsWidgetStyles.maxWidth), + alignment: AlignmentDirectional.center, + child: responsiveUtils.isScreenWithShortestSide(context) + ? SingleChildScrollView(child: childWidget) + : CustomScrollView( + slivers: [ + SliverFillRemaining(child: childWidget) + ] + ) + ); + } + + double _getIconSize(BuildContext context, ResponsiveUtils responsiveUtils) { + if (responsiveUtils.isMobile(context)) { + return EmptyEmailsWidgetStyles.mobileIconSize; + } else if (responsiveUtils.isDesktop(context)) { + return EmptyEmailsWidgetStyles.desktopIconSize; + } else { + return EmptyEmailsWidgetStyles.tabletIconSize; + } + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart b/lib/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart index d444618f4d..229663f872 100644 --- a/lib/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart +++ b/lib/features/thread/presentation/widgets/filter_message_cupertino_action_sheet_action_builder.dart @@ -34,7 +34,7 @@ class FilterMessageCupertinoActionSheetActionBuilder return Container( color: bgColor ?? Colors.white, child: MouseRegion( - cursor: BuildUtils.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, + cursor: PlatformInfo.isWeb ? MaterialStateMouseCursor.clickable : MouseCursor.defer, child: CupertinoActionSheetAction( key: key, child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/features/thread/presentation/widgets/search_app_bar_widget.dart b/lib/features/thread/presentation/widgets/search_app_bar_widget.dart index c07ff6c6ad..4b446bb764 100644 --- a/lib/features/thread/presentation/widgets/search_app_bar_widget.dart +++ b/lib/features/thread/presentation/widgets/search_app_bar_widget.dart @@ -1,5 +1,9 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/views/button/icon_button_web.dart'; +import 'package:core/presentation/views/text/text_field_builder.dart'; +import 'package:core/utils/direction_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; @@ -9,93 +13,61 @@ typedef OnClearTextSearchAction = Function(); typedef OnTextChangeSearchAction = Function(String); typedef OnSearchTextAction = Function(String); -class SearchAppBarWidget { - OnCancelSearchPressed? _onCancelSearchPressed; - OnTextChangeSearchAction? _onTextChangeSearchAction; - OnClearTextSearchAction? _onClearTextSearchAction; - OnSearchTextAction? _onSearchTextAction; +class SearchAppBarWidget extends StatelessWidget { - final ImagePaths _imagePaths; - final SearchQuery? _searchQuery; - final TextEditingController? _searchInputController; - final FocusNode? _searchFocusNode; + final ImagePaths imagePaths; + final SearchQuery? searchQuery; + final TextEditingController? searchInputController; + final FocusNode? searchFocusNode; final List? suggestionSearch; final bool hasBackButton; final bool hasSearchButton; - - double? _heightSearchBar; - Decoration? _decoration; - EdgeInsets? _padding; - EdgeInsets? _margin; - String? _hintText; - Widget? _iconClearText; - - SearchAppBarWidget( - this._imagePaths, - this._searchQuery, - this._searchFocusNode, - this._searchInputController, - { - this.hasBackButton = true, - this.suggestionSearch, - this.hasSearchButton = false, - } - ); - - void addOnCancelSearchPressed(OnCancelSearchPressed onCancelSearchPressed) { - _onCancelSearchPressed = onCancelSearchPressed; - } - - void addOnTextChangeSearchAction(OnTextChangeSearchAction onTextChangeSearchAction) { - _onTextChangeSearchAction = onTextChangeSearchAction; - } - - void addOnClearTextSearchAction(OnClearTextSearchAction onClearTextSearchAction) { - _onClearTextSearchAction = onClearTextSearchAction; - } - - void addOnSearchTextAction(OnSearchTextAction onSearchTextAction) { - _onSearchTextAction = onSearchTextAction; - } - - void setHeightSearchBar(double heightSearchBar) { - _heightSearchBar = heightSearchBar; - } - - void addDecoration(Decoration decoration) { - _decoration = decoration; - } - - void addPadding(EdgeInsets padding) { - _padding = padding; - } - - void setMargin(EdgeInsets margin) { - _margin = margin; - } - - void setHintText(String text) { - _hintText = text; - } - - void addIconClearText(Widget? icon) { - _iconClearText = icon; - } - - Widget build() { + final double? heightSearchBar; + final Decoration? decoration; + final EdgeInsets? padding; + final EdgeInsets? margin; + final String? hintText; + final Widget? iconClearText; + final OnCancelSearchPressed? onCancelSearchPressed; + final OnTextChangeSearchAction? onTextChangeSearchAction; + final OnClearTextSearchAction? onClearTextSearchAction; + final OnSearchTextAction? onSearchTextAction; + + const SearchAppBarWidget({ + super.key, + required this.imagePaths, + required this.hasBackButton, + required this.hasSearchButton, + this.searchQuery, + this.searchInputController, + this.searchFocusNode, + this.suggestionSearch, + this.heightSearchBar, + this.decoration, + this.padding, + this.margin, + this.hintText, + this.iconClearText, + this.onCancelSearchPressed, + this.onTextChangeSearchAction, + this.onClearTextSearchAction, + this.onSearchTextAction + }); + + @override + Widget build(BuildContext context) { return Container( - key: const Key('search_app_bar_widget'), - height: _heightSearchBar, - decoration: _decoration, - padding: _padding ?? EdgeInsets.zero, - margin: _margin, + key: key ?? const Key('search_app_bar_widget'), + height: heightSearchBar, + decoration: decoration, + padding: padding ?? EdgeInsets.zero, + margin: margin, child: Row( children: [ if (hasBackButton) _buildBackButton(), if (hasSearchButton) _buildSearchButton(), - Expanded(child: _buildSearchInputForm()), - if (suggestionSearch?.isNotEmpty == true - || (_searchQuery != null && _searchQuery!.value.isNotEmpty)) + Expanded(child: _buildSearchInputForm(context)), + if (suggestionSearch?.isNotEmpty == true || (searchQuery != null && searchQuery!.value.isNotEmpty)) _buildClearTextSearchButton(), ] ) @@ -104,44 +76,49 @@ class SearchAppBarWidget { Widget _buildBackButton() { return buildIconWeb( - icon: SvgPicture.asset(_imagePaths.icBack, color: AppColor.colorTextButton, fit: BoxFit.fill), - onTap: () { - _searchInputController?.clear(); - if (_onCancelSearchPressed != null) { - _onCancelSearchPressed!(); - } - }); + icon: SvgPicture.asset( + imagePaths.icBack, + colorFilter: AppColor.colorTextButton.asFilter(), + fit: BoxFit.fill), + onTap: () { + searchInputController?.clear(); + if (onCancelSearchPressed != null) { + onCancelSearchPressed!(); + } + }); } Widget _buildClearTextSearchButton() { return buildIconWeb( - icon: _iconClearText ?? SvgPicture.asset(_imagePaths.icComposerClose, width: 18, height: 18, fit: BoxFit.fill), - onTap: () { - _searchInputController?.clear(); - _onClearTextSearchAction?.call(); - }); + icon: iconClearText ?? SvgPicture.asset(imagePaths.icComposerClose, width: 18, height: 18, fit: BoxFit.fill), + onTap: () { + searchInputController?.clear(); + onClearTextSearchAction?.call(); + }); } - Widget _buildSearchInputForm() { - return (TextFieldBuilder() - ..key(const Key('search_input_form')) - ..textInputAction(TextInputAction.done) - ..onChange((value) => _onTextChangeSearchAction?.call(value)) - ..onSubmitted((value) => _onSearchTextAction?.call(value)) - ..cursorColor(AppColor.colorTextButton) - ..autoFocus(true) - ..addFocusNode(_searchFocusNode) - ..textStyle(const TextStyle(color: AppColor.colorNameEmail, fontSize: 17)) - ..textDecoration(InputDecoration( - border: InputBorder.none, - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - contentPadding: EdgeInsets.zero, - hintText: _hintText, - hintStyle: const TextStyle(color: AppColor.colorHintSearchBar, fontSize: 17.0), - labelStyle: const TextStyle(color: AppColor.colorHintSearchBar, fontSize: 17.0))) - ..addController(_searchInputController)) - .build(); + Widget _buildSearchInputForm(BuildContext context) { + return TextFieldBuilder( + key: const Key('search_input_form'), + textInputAction: TextInputAction.done, + onTextChange: onTextChangeSearchAction, + onTextSubmitted: onSearchTextAction, + cursorColor: AppColor.colorTextButton, + maxLines: 1, + textDirection: DirectionUtils.getDirectionByLanguage(context), + autoFocus: true, + focusNode: searchFocusNode, + textStyle: const TextStyle(color: AppColor.colorNameEmail, fontSize: 17), + decoration: InputDecoration( + border: InputBorder.none, + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + contentPadding: EdgeInsets.zero, + hintText: hintText, + hintStyle: const TextStyle(color: AppColor.colorHintSearchBar, fontSize: 17.0), + labelStyle: const TextStyle(color: AppColor.colorHintSearchBar, fontSize: 17.0)), + controller: searchInputController, + ); } Widget _buildSearchButton() { @@ -151,10 +128,10 @@ class SearchAppBarWidget { minSize: 40, iconPadding: EdgeInsets.zero, icon: SvgPicture.asset( - _imagePaths.icSearchBar, + imagePaths.icSearchBar, fit: BoxFit.fill ), - onTap: () => _onSearchTextAction?.call(_searchInputController?.text ?? '') + onTap: () => onSearchTextAction?.call(searchInputController?.text ?? '') ), ); } diff --git a/lib/features/thread/presentation/widgets/spam_banner/spam_report_banner_button_widget.dart b/lib/features/thread/presentation/widgets/spam_banner/spam_report_banner_button_widget.dart new file mode 100644 index 0000000000..eacd076a60 --- /dev/null +++ b/lib/features/thread/presentation/widgets/spam_banner/spam_report_banner_button_widget.dart @@ -0,0 +1,92 @@ + +import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/presentation/utils/style_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/spam_banner/spam_report_banner_button_styles.dart'; + +class SpamReportBannerButtonWidget extends StatelessWidget { + + final String label; + final Color labelColor; + final VoidCallback onTap; + final String? icon; + final bool iconLeftAlignment; + final bool wrapContent; + + const SpamReportBannerButtonWidget({ + super.key, + required this.label, + required this.labelColor, + required this.onTap, + this.icon, + this.iconLeftAlignment = true, + this.wrapContent = false, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: const BorderRadius.all(Radius.circular(SpamReportBannerButtonStyles.borderRadius)), + child: Container( + width: wrapContent ? null : double.infinity, + padding: const EdgeInsetsDirectional.all(SpamReportBannerButtonStyles.padding), + decoration: const BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(SpamReportBannerButtonStyles.borderRadius)), + color: SpamReportBannerButtonStyles.backgroundColor + ), + child: icon == null + ? Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: SpamReportBannerButtonStyles.labelTextSize, + color: labelColor, + fontWeight: FontWeight.w400 + ) + ) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (iconLeftAlignment) + Padding( + padding: const EdgeInsetsDirectional.only(end: SpamReportBannerButtonStyles.paddingIcon), + child: SvgPicture.asset( + icon!, + width: SpamReportBannerButtonStyles.iconSize, + height: SpamReportBannerButtonStyles.iconSize, + colorFilter: labelColor.asFilter(), + ), + ), + Text( + label, + textAlign: TextAlign.center, + overflow: CommonTextStyle.defaultTextOverFlow, + softWrap: CommonTextStyle.defaultSoftWrap, + style: TextStyle( + fontSize: SpamReportBannerButtonStyles.labelTextSize, + color: labelColor, + fontWeight: FontWeight.w400 + ) + ), + if (!iconLeftAlignment) + Padding( + padding: const EdgeInsetsDirectional.only(start: SpamReportBannerButtonStyles.paddingIcon), + child: SvgPicture.asset( + icon!, + width: SpamReportBannerButtonStyles.iconSize, + height: SpamReportBannerButtonStyles.iconSize, + colorFilter: labelColor.asFilter(), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/thread/presentation/widgets/spam_banner/spam_report_banner_label_widget.dart b/lib/features/thread/presentation/widgets/spam_banner/spam_report_banner_label_widget.dart new file mode 100644 index 0000000000..ed0d468620 --- /dev/null +++ b/lib/features/thread/presentation/widgets/spam_banner/spam_report_banner_label_widget.dart @@ -0,0 +1,59 @@ + +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/spam_banner/spam_report_banner_label_styles.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class SpamReportBannerLabelWidget extends StatelessWidget { + + final String countSpamEmailsAsString; + final Color labelColor; + + const SpamReportBannerLabelWidget({ + super.key, + required this.countSpamEmailsAsString, + this.labelColor = SpamReportBannerLabelStyles.labelTextColor + }); + + @override + Widget build(BuildContext context) { + final responsiveUtils = Get.find(); + final imagePaths = Get.find(); + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + imagePaths.icInfoCircleOutline, + width: SpamReportBannerLabelStyles.iconSize, + height: SpamReportBannerLabelStyles.iconSize + ), + const SizedBox(width: SpamReportBannerLabelStyles.space), + if (responsiveUtils.isWebDesktop(context)) + Text( + AppLocalizations.of(context).countNewSpamEmails(countSpamEmailsAsString), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: SpamReportBannerLabelStyles.labelTextSize, + color: labelColor, + fontWeight: FontWeight.w500 + ), + ) + else + Flexible( + child: Text( + AppLocalizations.of(context).countNewSpamEmails(countSpamEmailsAsString), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: SpamReportBannerLabelStyles.labelTextSize, + color: labelColor, + fontWeight: FontWeight.w500 + ), + ), + ) + ], + ); + } +} diff --git a/lib/features/thread/presentation/widgets/spam_banner/spam_report_banner_web_widget.dart b/lib/features/thread/presentation/widgets/spam_banner/spam_report_banner_web_widget.dart new file mode 100644 index 0000000000..a4e23f5964 --- /dev/null +++ b/lib/features/thread/presentation/widgets/spam_banner/spam_report_banner_web_widget.dart @@ -0,0 +1,85 @@ +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/utils/direction_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/spam_banner/spam_report_banner_button_styles.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/spam_banner/spam_report_banner_label_styles.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/spam_banner/spam_report_banner_web_styles.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/spam_banner/spam_report_banner_button_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/spam_banner/spam_report_banner_label_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class SpamReportBannerWebWidget extends StatelessWidget { + const SpamReportBannerWebWidget({ Key? key }) : super(key: key); + + @override + Widget build(BuildContext context){ + final spamReportController = Get.find(); + final imagePaths = Get.find(); + return Obx(() { + if (!spamReportController.enableSpamReport || spamReportController.notShowSpamReportBanner) { + return const SizedBox( + height: 8, + ); + } + return Container( + margin: const EdgeInsetsDirectional.only( + end: SpamReportBannerWebStyles.horizontalMargin, + top: SpamReportBannerWebStyles.verticalMargin, + ), + width: double.infinity, + padding: const EdgeInsetsDirectional.symmetric( + horizontal: SpamReportBannerWebStyles.horizontalPadding, + vertical: SpamReportBannerWebStyles.verticalPadding, + ), + decoration: ShapeDecoration( + color: SpamReportBannerWebStyles.backgroundColor, + shape: const RoundedRectangleBorder( + side: BorderSide( + width: 1, + color: SpamReportBannerWebStyles.strokeBorderColor, + ), + borderRadius: BorderRadius.all(Radius.circular(SpamReportBannerWebStyles.borderRadius)), + ), + ), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + SpamReportBannerLabelWidget( + countSpamEmailsAsString: spamReportController.numberOfUnreadSpamEmails, + labelColor: SpamReportBannerLabelStyles.highlightLabelTextColor + ), + const SizedBox(width: 32), + SpamReportBannerButtonWidget( + label: AppLocalizations.of(context).showDetails, + labelColor: SpamReportBannerButtonStyles.positiveButtonTextColor, + onTap: spamReportController.openMailbox, + icon: DirectionUtils.isDirectionRTLByLanguage(context) + ? imagePaths.icArrowLeft + : imagePaths.icArrowRight, + iconLeftAlignment: false, + wrapContent: true, + ), + ], + ), + PositionedDirectional( + end: 0, + child: SpamReportBannerButtonWidget( + label: AppLocalizations.of(context).dismiss, + labelColor: SpamReportBannerButtonStyles.negativeButtonTextColor, + onTap: () => spamReportController.dismissSpamReportAction(context), + icon: imagePaths.icClose, + wrapContent: true, + ), + ) + ], + ), + ); + }); + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/spam_banner/spam_report_banner_widget.dart b/lib/features/thread/presentation/widgets/spam_banner/spam_report_banner_widget.dart new file mode 100644 index 0000000000..35ddf9ca00 --- /dev/null +++ b/lib/features/thread/presentation/widgets/spam_banner/spam_report_banner_widget.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/spam_banner/spam_report_banner_button_styles.dart'; +import 'package:tmail_ui_user/features/thread/presentation/styles/spam_banner/spam_report_banner_styles.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/spam_banner/spam_report_banner_button_widget.dart'; +import 'package:tmail_ui_user/features/thread/presentation/widgets/spam_banner/spam_report_banner_label_widget.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; + +class SpamReportBannerWidget extends StatelessWidget { + const SpamReportBannerWidget({ Key? key }) : super(key: key); + + @override + Widget build(BuildContext context){ + final spamReportController = Get.find(); + + return Obx(() { + if (!spamReportController.enableSpamReport || spamReportController.notShowSpamReportBanner) { + return const SizedBox.shrink(); + } + return Container( + margin: const EdgeInsetsDirectional.only( + start: SpamReportBannerStyles.horizontalMargin, + end: SpamReportBannerStyles.horizontalMargin, + bottom: SpamReportBannerStyles.verticalMargin + ), + padding: const EdgeInsetsDirectional.all(SpamReportBannerStyles.padding), + decoration: ShapeDecoration( + color: SpamReportBannerStyles.backgroundColor, + shape: const RoundedRectangleBorder( + side: BorderSide( + width: 1, + color: SpamReportBannerStyles.strokeBorderColor, + ), + borderRadius: BorderRadius.all(Radius.circular(SpamReportBannerStyles.borderRadius)), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SpamReportBannerLabelWidget(countSpamEmailsAsString: spamReportController.numberOfUnreadSpamEmails), + const SizedBox(height: SpamReportBannerStyles.space), + Row( + children: [ + Expanded( + child: SpamReportBannerButtonWidget( + label: AppLocalizations.of(context).showDetails, + labelColor: SpamReportBannerButtonStyles.positiveButtonTextColor, + onTap: spamReportController.openMailbox + ), + ), + const SizedBox(width: SpamReportBannerStyles.space), + Expanded( + child: SpamReportBannerButtonWidget( + label: AppLocalizations.of(context).dismiss, + labelColor: SpamReportBannerButtonStyles.negativeButtonTextColor, + onTap: () => spamReportController.dismissSpamReportAction(context) + ), + ), + ], + ), + ], + ), + ); + }); + } +} \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/spam_report_banner_widget.dart b/lib/features/thread/presentation/widgets/spam_report_banner_widget.dart deleted file mode 100644 index b84550f3eb..0000000000 --- a/lib/features/thread/presentation/widgets/spam_report_banner_widget.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'package:core/presentation/extensions/color_extension.dart'; -import 'package:core/presentation/resources/image_paths.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/controller/spam_report_controller.dart'; -import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; - -class SpamReportBannerWidget extends StatelessWidget { - const SpamReportBannerWidget({ Key? key }) : super(key: key); - - @override - Widget build(BuildContext context){ - final _spamReportController = Get.find(); - final _imagePaths = Get.find(); - - return Obx(() { - if (!_spamReportController.enableSpamReport || _spamReportController.notShowSpamReportBanner) { - return const SizedBox.shrink(); - } - return Container( - height: 124, - margin: const EdgeInsets.only(left: 16, right: 16, bottom: 16), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColor.colorBorderBodyThread, width: 1), - color: AppColor.colorSpamReportBox.withOpacity(0.12)), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.only(top: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - _imagePaths.icInfoCircleOutline, - width: 28, - height: 28, - color: AppColor.primaryColor, - ), - const SizedBox( - width: 8, - ), - Text( - AppLocalizations.of(context).countNewSpamEmails(_spamReportController.numberOfUnreadSpamEmails), - style: const TextStyle( - fontSize: 16, - color: AppColor.primaryColor, - fontWeight: FontWeight.w500), - ), - ], - ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - child: _spamReportButtonAction( - context, - AppLocalizations.of(context).showDetails, - AppColor.primaryColor, - () => _spamReportController.openMailbox(context)), - ), - const SizedBox( - width: 8, - ), - Expanded( - child: _spamReportButtonAction( - context, - AppLocalizations.of(context).dismiss, - AppColor.textFieldErrorBorderColor, - () => _spamReportController - .dismissSpamReportAction()), - ), - ], - ), - ), - ), - ], - ), - ); - }); - } - - Widget _spamReportButtonAction(BuildContext context, String title, Color colorText, Function()? onTap) { - return GestureDetector( - onTap: onTap, - child: Container( - height: 36, - width: double.infinity, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: AppColor.colorCreateNewIdentityButton), - child: Center( - child: Text( - title, - style: TextStyle( - fontSize: 15, color: colorText, fontWeight: FontWeight.w400), - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/features/thread/presentation/widgets/thread_view_bottom_loading_bar_widget.dart b/lib/features/thread/presentation/widgets/thread_view_bottom_loading_bar_widget.dart new file mode 100644 index 0000000000..6440543e12 --- /dev/null +++ b/lib/features/thread/presentation/widgets/thread_view_bottom_loading_bar_widget.dart @@ -0,0 +1,33 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/widget/cupertino_loading_widget.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/load_more_emails_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/search_more_email_state.dart'; + +class ThreadViewBottomLoadingBarWidget extends StatelessWidget { + + final Either viewState; + + const ThreadViewBottomLoadingBarWidget({ + super.key, + required this.viewState, + }); + + @override + Widget build(BuildContext context) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is SearchingMoreState || success is LoadingMoreEmails) { + return const Padding( + padding: EdgeInsetsDirectional.only(bottom: 16), + child: CupertinoLoadingWidget()); + } else { + return const SizedBox.shrink(); + } + } + ); + } +} diff --git a/lib/features/thread/presentation/widgets/thread_view_loading_bar_widget.dart b/lib/features/thread/presentation/widgets/thread_view_loading_bar_widget.dart new file mode 100644 index 0000000000..236559210a --- /dev/null +++ b/lib/features/thread/presentation/widgets/thread_view_loading_bar_widget.dart @@ -0,0 +1,33 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:flutter/material.dart'; +import 'package:tmail_ui_user/features/base/widget/cupertino_loading_widget.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/get_all_email_state.dart'; +import 'package:tmail_ui_user/features/thread/domain/state/search_email_state.dart'; + +class ThreadViewLoadingBarWidget extends StatelessWidget { + + final Either viewState; + + const ThreadViewLoadingBarWidget({ + super.key, + required this.viewState, + }); + + @override + Widget build(BuildContext context) { + return viewState.fold( + (failure) => const SizedBox.shrink(), + (success) { + if (success is SearchingState || success is GetAllEmailLoading) { + return const Padding( + padding: EdgeInsetsDirectional.only(top: 16), + child: CupertinoLoadingWidget()); + } else { + return const SizedBox.shrink(); + } + } + ); + } +} diff --git a/lib/features/upload/data/datasource_impl/attachment_upload_datasource_impl.dart b/lib/features/upload/data/datasource_impl/attachment_upload_datasource_impl.dart index c6f3e4054f..f57f0262b1 100644 --- a/lib/features/upload/data/datasource_impl/attachment_upload_datasource_impl.dart +++ b/lib/features/upload/data/datasource_impl/attachment_upload_datasource_impl.dart @@ -26,8 +26,6 @@ class AttachmentUploadDataSourceImpl extends AttachmentUploadDataSource { _fileUploader, cancelToken: cancelToken )..upload(); - }).catchError((error) { - _exceptionThrower.throwException(error); - }); + }).catchError(_exceptionThrower.throwException); } } \ No newline at end of file diff --git a/lib/features/upload/data/model/upload_file_arguments.dart b/lib/features/upload/data/model/upload_file_arguments.dart index b580041009..fc4ad2b6d4 100644 --- a/lib/features/upload/data/model/upload_file_arguments.dart +++ b/lib/features/upload/data/model/upload_file_arguments.dart @@ -1,25 +1,28 @@ import 'package:core/data/network/dio_client.dart'; -import 'package:dio/dio.dart'; import 'package:equatable/equatable.dart'; -import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; class UploadFileArguments with EquatableMixin { final DioClient dioClient; final UploadTaskId uploadId; - final FileInfo fileInfo; + final MobileFileUpload mobileFileUpload; final Uri uploadUri; - final CancelToken? cancelToken; UploadFileArguments( this.dioClient, this.uploadId, - this.fileInfo, - this.uploadUri, - {this.cancelToken}); + this.mobileFileUpload, + this.uploadUri + ); @override - List get props => [uploadId, fileInfo, uploadUri]; + List get props => [ + dioClient, + uploadId, + mobileFileUpload, + uploadUri + ]; } \ No newline at end of file diff --git a/lib/features/upload/data/network/file_uploader.dart b/lib/features/upload/data/network/file_uploader.dart index 1b074b67b2..271b43fa06 100644 --- a/lib/features/upload/data/network/file_uploader.dart +++ b/lib/features/upload/data/network/file_uploader.dart @@ -7,7 +7,7 @@ import 'package:core/data/network/dio_client.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:model/email/attachment.dart'; @@ -15,6 +15,7 @@ import 'package:model/upload/file_info.dart'; import 'package:model/upload/upload_response.dart'; import 'package:tmail_ui_user/features/upload/data/model/upload_file_arguments.dart'; import 'package:tmail_ui_user/features/upload/domain/exceptions/upload_exception.dart'; +import 'package:tmail_ui_user/features/upload/domain/extensions/file_info_extension.dart'; import 'package:tmail_ui_user/features/upload/domain/model/upload_task_id.dart'; import 'package:tmail_ui_user/features/upload/domain/state/attachment_upload_state.dart'; import 'package:worker_manager/worker_manager.dart' as worker; @@ -33,7 +34,7 @@ class FileUploader { Uri uploadUri, {CancelToken? cancelToken} ) async { - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { return _handleUploadAttachmentActionOnWeb( uploadId, onSendController, @@ -41,13 +42,13 @@ class FileUploader { uploadUri, cancelToken: cancelToken); } else { + final mobileFileUpload = fileInfo.toMobileFileUpload(); return await _isolateExecutor.execute( arg1: UploadFileArguments( _dioClient, uploadId, - fileInfo, - uploadUri, - cancelToken: cancelToken + mobileFileUpload, + uploadUri ), fun1: _handleUploadAttachmentAction, notification: (value) { @@ -66,34 +67,31 @@ class FileUploader { UploadFileArguments argsUpload, worker.TypeSendPort sendPort ) async { - final dioClient = argsUpload.dioClient; - final fileInfo = argsUpload.fileInfo; - final uploadUri = argsUpload.uploadUri; - final cancelToken = argsUpload.cancelToken; + final headerParam = argsUpload.dioClient.getHeaders(); + headerParam[HttpHeaders.contentTypeHeader] = argsUpload.mobileFileUpload.mimeType; + headerParam[HttpHeaders.contentLengthHeader] = argsUpload.mobileFileUpload.fileSize; + final data = File(argsUpload.mobileFileUpload.filePath).openRead(); - final resultJson = await _invokeRequestToServer( - dioClient, - uploadUri, - fileInfo, - cancelToken: cancelToken, + final resultJson = await argsUpload.dioClient.post( + Uri.decodeFull(argsUpload.uploadUri.toString()), + options: Options(headers: headerParam), + data: data, onSendProgress: (count, total) { log('FileUploader::_handleUploadAttachmentAction():onSendProgress: [${argsUpload.uploadId.id}] = $count'); sendPort.send( UploadingAttachmentUploadState( argsUpload.uploadId, count, - fileInfo.fileSize + argsUpload.mobileFileUpload.fileSize ) ); } ); log('FileUploader::_handleUploadAttachmentAction():resultJson: $resultJson'); - if (cancelToken?.isCancelled == true) { - log('FileUploader::_handleUploadAttachmentAction(): upload is cancelled'); - return null; - } - - return _parsingResponse(resultJson: resultJson, fileName: fileInfo.fileName); + return _parsingResponse( + resultJson: resultJson, + fileName: argsUpload.mobileFileUpload.fileName + ); } Future _handleUploadAttachmentActionOnWeb( @@ -103,10 +101,15 @@ class FileUploader { Uri uploadUri, {CancelToken? cancelToken} ) async { - final resultJson = await _invokeRequestToServer( - _dioClient, - uploadUri, - fileInfo, + final headerParam = _dioClient.getHeaders(); + headerParam[HttpHeaders.contentTypeHeader] = fileInfo.mimeType; + headerParam[HttpHeaders.contentLengthHeader] = fileInfo.fileSize; + final data = fileInfo.readStream; + + final resultJson = await _dioClient.post( + Uri.decodeFull(uploadUri.toString()), + options: Options(headers: headerParam), + data: data, cancelToken: cancelToken, onSendProgress: (count, total) { log('FileUploader::_handleUploadAttachmentActionOnWeb():onSendProgress: [${uploadId.id}] = $count'); @@ -119,44 +122,10 @@ class FileUploader { ); } ); - log('FileUploader::_handleUploadAttachmentActionOnWeb():resultJson: $resultJson'); - - if (cancelToken?.isCancelled == true) { - log('FileUploader::_handleUploadAttachmentActionOnWeb(): upload is cancelled'); - return null; - } - return _parsingResponse(resultJson: resultJson, fileName: fileInfo.fileName); } - static Future _invokeRequestToServer( - DioClient dioClient, - Uri uploadUri, - FileInfo fileInfo, { - CancelToken? cancelToken, - ProgressCallback? onSendProgress - }) { - final headerParam = dioClient.getHeaders(); - headerParam[HttpHeaders.contentTypeHeader] = fileInfo.mimeType; - headerParam[HttpHeaders.contentLengthHeader] = fileInfo.fileSize; - - final data = fileInfo.readStream ?? File(fileInfo.filePath).openRead(); - - if (cancelToken?.isCancelled == true) { - log('FileUploader::_invokeRequestToServer(): upload is cancelled'); - return Future.value(); - } - - return dioClient.post( - Uri.decodeFull(uploadUri.toString()), - options: Options(headers: headerParam), - data: data, - cancelToken: cancelToken, - onSendProgress: onSendProgress - ); - } - static Attachment? _parsingResponse({dynamic resultJson, required String fileName}) { log('FileUploader::_parsingResponse():resultJson: $resultJson'); if (resultJson != null) { diff --git a/lib/features/upload/domain/extensions/file_info_extension.dart b/lib/features/upload/domain/extensions/file_info_extension.dart new file mode 100644 index 0000000000..58866921c4 --- /dev/null +++ b/lib/features/upload/domain/extensions/file_info_extension.dart @@ -0,0 +1,7 @@ + +import 'package:model/upload/file_info.dart'; +import 'package:tmail_ui_user/features/upload/domain/model/mobile_file_upload.dart'; + +extension FileInfoExtension on FileInfo { + MobileFileUpload toMobileFileUpload() => MobileFileUpload(fileName, filePath, fileSize, mimeType); +} \ No newline at end of file diff --git a/lib/features/upload/domain/model/mobile_file_upload.dart b/lib/features/upload/domain/model/mobile_file_upload.dart new file mode 100644 index 0000000000..72c63b3da4 --- /dev/null +++ b/lib/features/upload/domain/model/mobile_file_upload.dart @@ -0,0 +1,24 @@ + +import 'package:equatable/equatable.dart'; + +class MobileFileUpload with EquatableMixin { + final String fileName; + final String filePath; + final int fileSize; + final String mimeType; + + MobileFileUpload( + this.fileName, + this.filePath, + this.fileSize, + this.mimeType + ); + + @override + List get props => [ + fileName, + filePath, + fileSize, + mimeType, + ]; +} \ No newline at end of file diff --git a/lib/features/upload/domain/model/upload_attachment.dart b/lib/features/upload/domain/model/upload_attachment.dart index 2dd3e248e1..93fa86e8b7 100644 --- a/lib/features/upload/domain/model/upload_attachment.dart +++ b/lib/features/upload/domain/model/upload_attachment.dart @@ -57,11 +57,15 @@ class UploadAttachment with EquatableMixin { if (attachment != null) { _updateEvent(Right(SuccessAttachmentUploadState(uploadTaskId, attachment, fileInfo))); } else { - _updateEvent(Left(ErrorAttachmentUploadState(uploadTaskId))); + _updateEvent(Left(ErrorAttachmentUploadState(uploadId: uploadTaskId))); } } catch (e) { logError('UploadAttachment::upload():ERROR: $e'); - _updateEvent(Left(ErrorAttachmentUploadState(uploadTaskId))); + if (e is DioError && e.type == DioErrorType.cancel) { + _updateEvent(Left(CancelAttachmentUploadState(uploadTaskId))); + } else { + _updateEvent(Left(ErrorAttachmentUploadState(uploadId: uploadTaskId, exception: e))); + } } finally { await _progressStateController.close(); } diff --git a/lib/features/upload/domain/state/attachment_upload_state.dart b/lib/features/upload/domain/state/attachment_upload_state.dart index c4794f6abb..b5f0ee2f57 100644 --- a/lib/features/upload/domain/state/attachment_upload_state.dart +++ b/lib/features/upload/domain/state/attachment_upload_state.dart @@ -13,7 +13,7 @@ class PendingAttachmentUploadState extends Success { PendingAttachmentUploadState(this.uploadId, this.progress, this.total); @override - List get props => [progress, total]; + List get props => [uploadId, progress, total]; } class UploadingAttachmentUploadState extends Success { @@ -24,27 +24,43 @@ class UploadingAttachmentUploadState extends Success { UploadingAttachmentUploadState(this.uploadId, this.progress, this.total); @override - List get props => [progress, total]; + List get props => [uploadId, progress, total]; } class SuccessAttachmentUploadState extends Success { final UploadTaskId uploadId; final Attachment attachment; final FileInfo fileInfo; + final bool fromFileShared; - SuccessAttachmentUploadState(this.uploadId, this.attachment, this.fileInfo); + SuccessAttachmentUploadState( + this.uploadId, + this.attachment, + this.fileInfo, + { + this.fromFileShared = false + } + ); @override - List get props => [uploadId, attachment, fileInfo]; + List get props => [ + uploadId, + attachment, + fileInfo, + fromFileShared, + ]; } -class ErrorAttachmentUploadState extends Failure { +class ErrorAttachmentUploadState extends FeatureFailure { final UploadTaskId uploadId; - ErrorAttachmentUploadState(this.uploadId); + ErrorAttachmentUploadState({ + required this.uploadId, + dynamic exception + }) : super(exception: exception); @override - List get props => [uploadId]; + List get props => [uploadId, ...super.props]; } class CancelAttachmentUploadState extends Failure { diff --git a/lib/features/upload/domain/state/local_file_picker_state.dart b/lib/features/upload/domain/state/local_file_picker_state.dart index e4a943b23b..a2d948fe58 100644 --- a/lib/features/upload/domain/state/local_file_picker_state.dart +++ b/lib/features/upload/domain/state/local_file_picker_state.dart @@ -1,6 +1,6 @@ - -import 'package:core/core.dart'; -import 'package:model/model.dart'; +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/upload/file_info.dart'; class LocalFilePickerSuccess extends UIState { final List pickedFiles; @@ -12,15 +12,8 @@ class LocalFilePickerSuccess extends UIState { } class LocalFilePickerFailure extends FeatureFailure { - final exception; - - LocalFilePickerFailure(this.exception); - @override - List get props => [exception]; + LocalFilePickerFailure(dynamic exception) : super(exception: exception); } -class LocalFilePickerCancel extends FeatureFailure { - @override - List get props => []; -} \ No newline at end of file +class LocalFilePickerCancel extends FeatureFailure {} \ No newline at end of file diff --git a/lib/features/upload/presentation/controller/upload_controller.dart b/lib/features/upload/presentation/controller/upload_controller.dart index 5940507c9d..60e2fedf8e 100644 --- a/lib/features/upload/presentation/controller/upload_controller.dart +++ b/lib/features/upload/presentation/controller/upload_controller.dart @@ -1,7 +1,6 @@ import 'package:async/async.dart'; import 'package:collection/collection.dart'; -import 'package:core/presentation/extensions/color_extension.dart'; import 'package:core/presentation/resources/image_paths.dart'; import 'package:core/presentation/state/failure.dart'; import 'package:core/presentation/state/success.dart'; @@ -9,6 +8,7 @@ import 'package:core/presentation/utils/app_toast.dart'; import 'package:core/utils/app_logger.dart'; import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; import 'package:model/email/attachment.dart'; @@ -128,13 +128,13 @@ class UploadController extends BaseController { if (failure is ErrorAttachmentUploadState) { uploadInlineViewState.value = Left(failure); _deleteInlineFileUploaded(failure.uploadId); + if (currentContext != null && currentOverlayContext != null) { - _appToast.showToastWithIcon(currentOverlayContext!, - message: AppLocalizations.of(currentContext!).thisImageCannotBeAdded, - textColor: AppColor.toastErrorBackgroundColor, - iconColor: AppColor.toastErrorBackgroundColor, - icon: _imagePaths.icInsertImage - ); + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).thisImageCannotBeAdded, + leadingSVGIconColor: Colors.white, + leadingSVGIcon: _imagePaths.icInsertImage); } } }, @@ -157,8 +157,13 @@ class UploadController extends BaseController { } else if (success is SuccessAttachmentUploadState) { log('UploadController::_handleProgressUploadInlineImageStateStream():succeed[${success.uploadId}]'); final inlineAttachment = success.attachment.toAttachmentWithDisposition( - disposition: ContentDisposition.inline, - cid: _uuid.v1()); + disposition: ContentDisposition.inline, + cid: _uuid.v1() + ); + + final uploadFileState = _uploadingStateInlineFiles.getUploadFileStateById(success.uploadId); + log('UploadController::_handleProgressUploadInlineImageStateStream:uploadId: ${uploadFileState?.uploadTaskId} | fromFileShared: ${uploadFileState?.fromFileShared}'); + _uploadingStateInlineFiles.updateElementByUploadTaskId( success.uploadId, (currentState) { @@ -173,7 +178,9 @@ class UploadController extends BaseController { final newUploadSuccess = SuccessAttachmentUploadState( success.uploadId, inlineAttachment, - success.fileInfo); + success.fileInfo, + fromFileShared: uploadFileState?.fromFileShared ?? false + ); _handleUploadInlineAttachmentsSuccess(newUploadSuccess); } } @@ -202,13 +209,21 @@ class UploadController extends BaseController { }); } - Future uploadFileAction(FileInfo uploadFile, Uri uploadUri, {bool isInline = false}) { - log('UploadController::_uploadFile():fileName: ${uploadFile.fileName}'); + Future uploadFileAction( + FileInfo uploadFile, + Uri uploadUri, + { + bool isInline = false, + bool fromFileShared = false, + } + ) { + log('UploadController::_uploadFile():fileName: ${uploadFile.fileName} | isInline: $isInline | fromFileShared: $fromFileShared'); consumeState(_uploadAttachmentInteractor.execute( uploadFile, uploadUri, cancelToken: CancelToken(), - isInline: isInline + isInline: isInline, + fromFileShared: fromFileShared )); return Future.value(); } @@ -241,21 +256,22 @@ class UploadController extends BaseController { void _handleUploadAttachmentsFailure(ErrorAttachmentUploadState failure) { if (currentContext != null && currentOverlayContext != null) { - _appToast.showToastWithIcon(currentOverlayContext!, - message: AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments, - textColor: AppColor.toastErrorBackgroundColor, - iconColor: AppColor.toastErrorBackgroundColor, - icon: _imagePaths.icAttachment); + _appToast.showToastErrorMessage( + currentOverlayContext!, + '${AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments}. ${failure.exception ?? ''}', + leadingSVGIconColor: Colors.white, + leadingSVGIcon: _imagePaths.icAttachment); } } void _handleUploadAttachmentsSuccess(SuccessAttachmentUploadState success) { log('UploadController::_handleUploadAttachmentsSuccess(): $success'); if (currentContext != null && currentOverlayContext != null && _uploadingStateFiles.allSuccess) { - _appToast.showToastWithIcon(currentOverlayContext!, - message: AppLocalizations.of(currentContext!).attachments_uploaded_successfully, - iconColor: AppColor.primaryColor, - icon: _imagePaths.icAttachment); + _appToast.showToastSuccessMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).attachments_uploaded_successfully, + leadingSVGIconColor: Colors.white, + leadingSVGIcon: _imagePaths.icAttachment); } } @@ -339,40 +355,35 @@ class UploadController extends BaseController { } @override - void onDone() { - viewState.value.fold(_handleFailureViewState, _handleSuccessViewState); - } - - void _handleFailureViewState(Failure failure) async { - logError('UploadController::_handleFailureViewState():failure: $failure'); + void handleFailureViewState(Failure failure) async { + super.handleFailureViewState(failure); if (failure is UploadAttachmentFailure) { if (failure.isInline) { if (currentContext != null && currentOverlayContext != null) { - _appToast.showToastWithIcon(currentOverlayContext!, - message: AppLocalizations.of(currentContext!).thisImageCannotBeAdded, - textColor: AppColor.toastErrorBackgroundColor, - iconColor: AppColor.toastErrorBackgroundColor, - icon: _imagePaths.icInsertImage - ); + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).thisImageCannotBeAdded, + leadingSVGIconColor: Colors.white, + leadingSVGIcon: _imagePaths.icInsertImage); } } else { if (currentContext != null && currentOverlayContext != null) { - _appToast.showToastWithIcon(currentOverlayContext!, - message: AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments, - textColor: AppColor.toastErrorBackgroundColor, - iconColor: AppColor.toastErrorBackgroundColor, - icon: _imagePaths.icAttachment - ); + _appToast.showToastErrorMessage( + currentOverlayContext!, + AppLocalizations.of(currentContext!).can_not_upload_this_file_as_attachments, + leadingSVGIconColor: Colors.white, + leadingSVGIcon: _imagePaths.icAttachment); } } } } - void _handleSuccessViewState(Success success) async { - log('UploadController::_handleSuccessViewState():success: $success'); + @override + void handleSuccessViewState(Success success) async { + super.handleSuccessViewState(success); if (success is UploadAttachmentSuccess) { if (success.isInline) { - _uploadingStateInlineFiles.add(success.uploadAttachment.toUploadFileState()); + _uploadingStateInlineFiles.add(success.uploadAttachment.toUploadFileState(fromFileShared: success.fromFileShared)); await _progressUploadInlineImageStateStreamGroup.add(success.uploadAttachment.progressState); } else { _uploadingStateFiles.add(success.uploadAttachment.toUploadFileState()); diff --git a/lib/features/upload/presentation/extensions/upload_attachment_extension.dart b/lib/features/upload/presentation/extensions/upload_attachment_extension.dart index b03f846b99..4b45c9427b 100644 --- a/lib/features/upload/presentation/extensions/upload_attachment_extension.dart +++ b/lib/features/upload/presentation/extensions/upload_attachment_extension.dart @@ -4,11 +4,12 @@ import 'package:tmail_ui_user/features/upload/presentation/model/upload_file_sta extension UploadAttachmentExtension on UploadAttachment { - UploadFileState toUploadFileState() { + UploadFileState toUploadFileState({bool fromFileShared = false}) { return UploadFileState( uploadTaskId, file: fileInfo, - cancelToken: cancelToken + cancelToken: cancelToken, + fromFileShared: fromFileShared, ); } } \ No newline at end of file diff --git a/lib/features/upload/presentation/model/upload_file_state.dart b/lib/features/upload/presentation/model/upload_file_state.dart index 17174f6d91..8b4f67c3a9 100644 --- a/lib/features/upload/presentation/model/upload_file_state.dart +++ b/lib/features/upload/presentation/model/upload_file_state.dart @@ -18,14 +18,19 @@ class UploadFileState with EquatableMixin { final int uploadingProgress; final Attachment? attachment; final CancelToken? cancelToken; + final bool fromFileShared; - UploadFileState(this.uploadTaskId, { - this.file, - this.uploadStatus = UploadFileStatus.waiting, - this.uploadingProgress = 0, - this.attachment, - this.cancelToken, - }); + UploadFileState( + this.uploadTaskId, + { + this.file, + this.uploadStatus = UploadFileStatus.waiting, + this.uploadingProgress = 0, + this.attachment, + this.cancelToken, + this.fromFileShared = false, + } + ); UploadFileState copyWith({ UploadTaskId? uploadTaskId, @@ -33,7 +38,8 @@ class UploadFileState with EquatableMixin { UploadFileStatus? uploadStatus, int? uploadingProgress, Attachment? attachment, - CancelToken? cancelToken + CancelToken? cancelToken, + bool? fromFileShared, }) { return UploadFileState( uploadTaskId ?? this.uploadTaskId, @@ -41,7 +47,8 @@ class UploadFileState with EquatableMixin { uploadStatus: uploadStatus ?? this.uploadStatus, uploadingProgress: uploadingProgress ?? this.uploadingProgress, attachment: attachment ?? this.attachment, - cancelToken: cancelToken ?? this.cancelToken + cancelToken: cancelToken ?? this.cancelToken, + fromFileShared: fromFileShared ?? this.fromFileShared ); } @@ -96,6 +103,8 @@ class UploadFileState with EquatableMixin { file, uploadStatus, uploadingProgress, - attachment + attachment, + cancelToken, + fromFileShared, ]; } diff --git a/lib/features/upload/presentation/model/upload_file_state_list.dart b/lib/features/upload/presentation/model/upload_file_state_list.dart index 940a8087c3..9529455dbb 100644 --- a/lib/features/upload/presentation/model/upload_file_state_list.dart +++ b/lib/features/upload/presentation/model/upload_file_state_list.dart @@ -66,4 +66,8 @@ class UploadFileStateList { _uploadingStateFiles.remove(fileState); } } + + UploadFileState? getUploadFileStateById(UploadTaskId uploadTaskId) { + return _uploadingStateFiles.firstWhereOrNull((fileState) => fileState?.uploadTaskId == uploadTaskId); + } } \ No newline at end of file diff --git a/lib/l10n/intl_ar.arb b/lib/l10n/intl_ar.arb new file mode 100644 index 0000000000..732cd9990a --- /dev/null +++ b/lib/l10n/intl_ar.arb @@ -0,0 +1,2802 @@ +{ + "Bad credentials": "بيانات اعتماد غير صحيحة.", + "@Bad credentials": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "loginInputUrlMessage": "لتسجيل الدخول والوصول إلى رسائلك، يرجى الاتصال بخادم JMAP الخاص بك.", + "@loginInputUrlMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "unknownError": "حدث خطأ غير معروف، يرجى المحاولة مرة أخرى.", + "@unknownError": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "encryptedMailbox": "صندوق بريد مشفر", + "@encryptedMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mark_as_starred": "وضع علامة كمفضلة", + "@mark_as_starred": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mark_as_unread": "وضع علامة كغير مقروء", + "@mark_as_unread": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "move_to_spam": "نقل إلى البريد المزعج (سبام)", + "@move_to_spam": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "marked_multiple_item_as_read": "تم وضع علامة كمقروء لـ {count} عنصر", + "@marked_multiple_item_as_read": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "with_attachments": "مع المرفقات", + "@with_attachments": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "disable_filter_message_toast": "لقد قمت بتعطيل تصفية الرسائل.", + "@disable_filter_message_toast": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "can_not_upload_this_file_as_attachments": "لا يمكن رفع هذا الملف كمرفقات.", + "@can_not_upload_this_file_as_attachments": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "edit": "تعديل", + "@edit": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "spam": "بريد مزعج (سبام)", + "@spam": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "the_feature_is_under_development": "هذه الميزة قيد التطوير.", + "@the_feature_is_under_development": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "marked_message_toast": "لقد قمت بوضع علامة على الرسائل بوضعية \"{action}\".", + "@marked_message_toast": { + "type": "text", + "placeholders_order": [ + "action" + ], + "placeholders": { + "action": {} + } + }, + "filter_messages": "تصفية الرسائل", + "@filter_messages": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "minimize": "تصغير", + "@minimize": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "more": "المزيد", + "@more": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "message_dialog_send_email_with_email_address_invalid": "تحقق من صحة عناوين البريد الإلكتروني وحاول مرة أخرى", + "@message_dialog_send_email_with_email_address_invalid": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "empty_trash_dialog_message": "أنت على وشك حذف جميع العناصر في سلة المهملات بشكل نهائي. هل ترغب في المتابعة؟", + "@empty_trash_dialog_message": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delete_single_message_dialog": "أنت على وشك حذف هذه الرسالة بشكل دائم. هل ترغب في المتابعة؟", + "@delete_single_message_dialog": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delete_message_forever": "حذف الرسالة بشكل دائم", + "@delete_message_forever": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "message_dialog_send_email_exceeds_maximum_size": "لا يمكن إرسال الرسالة الخاصة بك لأنها تتجاوز الحجم الأقصى المسموح به والذي يبلغ {maxSize}", + "@message_dialog_send_email_exceeds_maximum_size": { + "type": "text", + "placeholders_order": [ + "maxSize" + ], + "placeholders": { + "maxSize": {} + } + }, + "delete_multiple_messages_dialog": "أنت على وشك حذف {count} عناصر بشكل دائم في صندوق البريد {mailboxName}. هل ترغب في المتابعة؟", + "@delete_multiple_messages_dialog": { + "type": "text", + "placeholders_order": [ + "count", + "mailboxName" + ], + "placeholders": { + "count": {}, + "mailboxName": {} + } + }, + "message_dialog_upload_attachments_exceeds_maximum_size": "لقد وصلت إلى الحد الأقصى لحجم الملف. يُرجى تحميل ملفات بإجمالي حجم أقل من {maxSize}", + "@message_dialog_upload_attachments_exceeds_maximum_size": { + "type": "text", + "placeholders_order": [ + "maxSize" + ], + "placeholders": { + "maxSize": {} + } + }, + "remove_from_spam": "إزالة من الرسائل غير المرغوب فيها (سبام)", + "@remove_from_spam": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "marked_as_not_spam": "تم وضع علامة كرسالة غير مرغوب فيها (سبام)", + "@marked_as_not_spam": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "you_have_created_a_new_default_identity": "لقد قمت بإنشاء هوية افتراضية جديدة", + "@you_have_created_a_new_default_identity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "all_identities": "كل الهويات", + "@all_identities": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delete_failed": "فشل الحذف", + "@delete_failed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageMarkAsMailboxReadHasSomeEmailFailure": "لقد وضعت علامة على {count} الرسائل في \"{mailboxName}\" كمقروءة", + "@toastMessageMarkAsMailboxReadHasSomeEmailFailure": { + "type": "text", + "placeholders_order": [ + "mailboxName", + "count" + ], + "placeholders": { + "mailboxName": {}, + "count": {} + } + }, + "lastYears": "السنوات الاخيرة", + "@lastYears": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "last6Months": "آخر 6 أشهر", + "@last6Months": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteMailbox": "حذف صندوق البريد", + "@deleteMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "html": "Html", + "@html": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noPreviewAvailable": "لا تتوفر معاينة", + "@noPreviewAvailable": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "wrongUrlMessage": "عنوان URL للخادم غير صالح ، يرجى المحاولة مرة أخرى", + "@wrongUrlMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "allTime": "كل الوقت", + "@allTime": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "search": "البحث", + "@search": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveAndClose": "احفظ وأغلق", + "@saveAndClose": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "insertImageErrorDuplicate": "الرجاء إدخال صورة أو عنوان URL للصورة ، وليس كلاهما", + "@insertImageErrorDuplicate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "formatStrikethrough": "يتوسطه خط", + "@formatStrikethrough": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "chooseAColor": "اختيار اللون", + "@chooseAColor": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "formatTextBackgroundColor": "لون خلفية النص", + "@formatTextBackgroundColor": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "formatBold": "Bold", + "@formatBold": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "formatTextColor": "لون الخط", + "@formatTextColor": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "numberedList": "قائمة مُرقمة", + "@numberedList": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "conditionTitleRulesFilter": "إذا تم استيفاء جميع الشروط التالية:", + "@conditionTitleRulesFilter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "notContains": "لا يحتوي على", + "@notContains": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "moveMessage": "نقل الرسالة", + "@moveMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "notExactlyEquals": "لا يساوي بالضبط", + "@notExactlyEquals": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "ruleFilterAddressToField": "إلى", + "@ruleFilterAddressToField": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "ruleFilterAddressCcField": "Cc", + "@ruleFilterAddressCcField": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "newFilterWasCreated": "تم إنشاء فلتر جديد", + "@newFilterWasCreated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "headerRecipients": "المستلمون", + "@headerRecipients": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "forwarding": "إعادة توجيه", + "@forwarding": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacation": "أجازة", + "@vacation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "yourFilterHasBeenUpdated": "تم تحديث الفلتر الخاص بك", + "@yourFilterHasBeenUpdated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "actionTitleRulesFilter": "قم بالإجراء التالي:", + "@actionTitleRulesFilter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toMailbox": "إلى صندوق البريد:", + "@toMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageAddRecipientsSuccessfully": "تمت إضافة رسائل البريد الإلكتروني من قائمة المستلمين.", + "@toastMessageAddRecipientsSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "ruleFilterAddressFromField": "من", + "@ruleFilterAddressFromField": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEnableVacationResponderAutomatically": "سيتم تنشيط المجيب التلقائي في {startDate}", + "@messageEnableVacationResponderAutomatically": { + "type": "text", + "placeholders_order": [ + "startDate" + ], + "placeholders": { + "startDate": {} + } + }, + "hintInputAutocompleteContact": "أدخل الاسم أو عنوان البريد الإلكتروني", + "@hintInputAutocompleteContact": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "startTime": "وقت البدء", + "@startTime": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageArabic": "اللغة العربية", + "@languageArabic": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageItalian": "اللغة الايطالية", + "@languageItalian": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "urlLink": "URL", + "@urlLink": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "insert": "إدراج", + "@insert": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "insertImageErrorFileEmpty": "يرجى إما اختيار صورة أو إدخال عنوان URL للصورة", + "@insertImageErrorFileEmpty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "resetToDefault": "إعادة التعيين إلى الوضع الافتراضي", + "@resetToDefault": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "setColor": "اضبط اللون", + "@setColor": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "formatItalic": "Italic", + "@formatItalic": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "formatUnderline": "تسطير", + "@formatUnderline": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "codeView": "عرض التعليمات البرمجية", + "@codeView": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "fontFamily": "Font Family", + "@fontFamily": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "indent": "مسافة بادئة", + "@indent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "orderList": "قائمة مرتبة", + "@orderList": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "moveTo": "الانتقال إلى", + "@moveTo": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emailRules": "قواعد البريد الإلكتروني", + "@emailRules": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "headerNameOfRules": "اسم القواعد", + "@headerNameOfRules": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "editRule": "تعديل القاعدة", + "@editRule": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteRule": "حذف القاعدة", + "@deleteRule": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "createNewRule": "أنشئ قاعدة جديدة", + "@createNewRule": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "conditionValueHintTextInput": "القيمة", + "@conditionValueHintTextInput": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "recipient": "المتلقي", + "@recipient": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "contains": "يتضمن", + "@contains": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "activated": "مفعل", + "@activated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deactivated": "معطل", + "@deactivated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "startDate": "تاريخ البدء", + "@startDate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "endDate": "تاريخ الانتهاء", + "@endDate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacationStopsAt": "تتوقف العطلة في", + "@vacationStopsAt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "message": "الرسالة", + "@message": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hintMessageBodyVacation": "رسائل الاجازة", + "@hintMessageBodyVacation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noStartTime": "ليس هناك وقت بدء", + "@noStartTime": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEndTime": "ليس هناك وقت انتهاء", + "@noEndTime": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEndDate": "لا يوجد تاريخ انتهاء", + "@noEndDate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "errorMessageWhenStartDateVacationIsEmpty": "الرجاء إدخال تاريخ بدء صالح", + "@errorMessageWhenStartDateVacationIsEmpty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "errorMessageWhenEndDateVacationIsInValid": "يجب أن يكون تاريخ الانتهاء أكبر من تاريخ البدء", + "@errorMessageWhenEndDateVacationIsInValid": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDisableVacationResponderAutomatically": "توقف المجيب التلقائي في {endDate}", + "@messageDisableVacationResponderAutomatically": { + "type": "text", + "placeholders_order": [ + "endDate" + ], + "placeholders": { + "endDate": {} + } + }, + "messageConfirmationDialogDeleteRecipientForward": "هل تريد حذف البريد الإلكتروني {emailAddress}؟", + "@messageConfirmationDialogDeleteRecipientForward": { + "type": "text", + "placeholders_order": [ + "emailAddress" + ], + "placeholders": { + "emailAddress": {} + } + }, + "toastMessageDeleteRecipientSuccessfully": "تمت إزالة البريد الإلكتروني من قائمة المستلمين.", + "@toastMessageDeleteRecipientSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageLocalCopyEnable": "الاحتفاظ بالنسخة المحلية ممكنة.", + "@toastMessageLocalCopyEnable": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageLocalCopyDisable": "إبقاء النسخة المحلية معطلة.", + "@toastMessageLocalCopyDisable": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "keepLocalCopyForwardLabel": "احتفظ بنسخة من البريد الإلكتروني في البريد الوارد", + "@keepLocalCopyForwardLabel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emailRuleSettingExplanation": "إنشاء قواعد للتعامل مع الرسائل الواردة. يمكنك اختيار كل من الشرط الذي يؤدي إلى تشغيل القاعدة والإجراءات التي ستتخذها القاعدة.", + "@emailRuleSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageConfirmationDialogDeleteEmailRule": "هل تريد حذف القاعدة \"{ruleName}\"؟", + "@messageConfirmationDialogDeleteEmailRule": { + "type": "text", + "placeholders_order": [ + "ruleName" + ], + "placeholders": { + "ruleName": {} + } + }, + "deleteEmailRule": "حذف القاعدة", + "@deleteEmailRule": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastErrorMessageWhenCreateNewRule": "أنت لم تملأ المعلومات بالكامل.", + "@toastErrorMessageWhenCreateNewRule": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacationSettingExplanation": "يرسل رد آلي على الرسائل الواردة.", + "@vacationSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacationSettingToggleButtonAutoReply": "الرد تلقائيًا على الرسائل عند استلامها.", + "@vacationSettingToggleButtonAutoReply": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "endTime": "وقت النهاية", + "@endTime": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hintSubjectInputVacationSetting": "أدخل الموضوع", + "@hintSubjectInputVacationSetting": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveChanges": "احفظ التغييرات", + "@saveChanges": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageIsRequired": "الرسالة مطلوبة", + "@messageIsRequired": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "email": "البريد الإلكتروني", + "@email": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "initializing_data": "جاري تهيئة البيانات...", + "@initializing_data": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "login_text_slogan": "Team Mail", + "@login_text_slogan": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "loginInputCredentialMessage": "أدخِل بيانات الاعتماد الخاصة بك لتسجيل الدخول.", + "@loginInputCredentialMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "next": "التالي", + "@next": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "prefix_https": "//:https", + "@prefix_https": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "password": "كلمة المرور", + "@password": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "login": "تسجيل الدخول", + "@login": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "signIn": "تسجيل الدخول", + "@signIn": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "requiredEmail": "البريد الإلكتروني مطلوب", + "@requiredEmail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "requiredPassword": "كلمة المرور مطلوبة", + "@requiredPassword": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "search_folder": "البحث في المجلد", + "@search_folder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hint_content_email_composer": "ابدأ كتابة رسالتك البريدية هنا.", + "@hint_content_email_composer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "prefix_reply_email": ":Re", + "@prefix_reply_email": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mark_as_read": "وضع علامة كمقروء", + "@mark_as_read": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "an_error_occurred": "خطأ! حدث خطأ. يرجى المحاولة مرة أخرى في وقت لاحق.", + "@an_error_occurred": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleHeaderAttachment": "{count} المرفقات ({totalSize}):", + "@titleHeaderAttachment": { + "type": "text", + "placeholders_order": [ + "count", + "totalSize" + ], + "placeholders": { + "count": {}, + "totalSize": {} + } + }, + "user_cancel_download_file": "تم إلغاء تنزيل الملف بواسطة المستخدم.", + "@user_cancel_download_file": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "unread": "غير مقروءة", + "@unread": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "read": "مقروءة", + "@read": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "message_confirmation_dialog_delete_mailbox": "سيتم حذف صندوق البريد \"{nameMailbox}\" وجميع المجلدات الفرعية والرسائل التي يحتويها، ولن يكون بإمكانك استعادتها. هل ترغب في المتابعة وحذفه؟", + "@message_confirmation_dialog_delete_mailbox": { + "type": "text", + "placeholders_order": [ + "nameMailbox" + ], + "placeholders": { + "nameMailbox": {} + } + }, + "rename_mailbox": "إعادة تسمية صندوق البريد", + "@rename_mailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "this_field_cannot_be_blank": "هذا الحقل لا يمكن أن يكون فارغًا", + "@this_field_cannot_be_blank": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delete_mailboxes_failure": "فشل حذف صناديق البريد", + "@delete_mailboxes_failure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delete_mailboxes": "حذف صناديق البريد", + "@delete_mailboxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "new_message": "رسالة جديدة", + "@new_message": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hide": "إخفاء", + "@hide": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "send": "إرسال", + "@send": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attachments": "المرفقات", + "@attachments": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mark_all_as_read": "وضع علامة مقروءة على الكل", + "@mark_all_as_read": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "back": "رجوع", + "@back": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attach_file": "إرفاق ملف", + "@attach_file": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "add_recipients": "إضافة المستلمين", + "@add_recipients": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "show": "عرض", + "@show": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sending_failed": "فشل إرسال الرسالة", + "@sending_failed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "empty_subject": "عنوان فارغ", + "@empty_subject": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "send_anyway": "إرسال على أي حال", + "@send_anyway": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "moved_to_trash": "تم نقلها إلى سلة المهملات", + "@moved_to_trash": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "no_internet_connection": "لا يوجد اتصال بالإنترنت", + "@no_internet_connection": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "empty_trash_now": "إفراغ سلة المهملات الآن", + "@empty_trash_now": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "message_delete_all_email_in_trash_button": "سيتم حذف جميع الرسائل في سلة المهملات إذا تجاوزت السعة المحدودة.", + "@message_delete_all_email_in_trash_button": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "empty_trash_folder": "تفريغ مجلد البريد المهملات", + "@empty_trash_folder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delete_all": "حذف الكل", + "@delete_all": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "page_name": "Team Mail", + "@page_name": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "exchange": "تبادل", + "@exchange": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "got_it": "فهمت", + "@got_it": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "maximum_files_size": "الحجم الأقصى للملفات", + "@maximum_files_size": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "move_message": "نقل الرسالة", + "@move_message": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "forwarded_message": "الرسالة المُعاد توجيها", + "@forwarded_message": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "date": "التاريخ", + "@date": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mark_as_spam": "وضع علامة كرسالة غير مرغوب فيها (سبام)", + "@mark_as_spam": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "marked_as_spam": "تم وضع علامة كرسالة غير مرغوب فيها (سبام)", + "@marked_as_spam": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "star": "وضع علامة نجمة", + "@star": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "signature": "التوقيع", + "@signature": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "plain_text": "نص عادي", + "@plain_text": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "reply_to": "الرد على", + "@reply_to": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bcc_to": "Bcc to", + "@bcc_to": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "fromMe": "مني", + "@fromMe": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "recent": "مؤخرًا", + "@recent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "nameOrEmailAddress": "الاسم أو عنوان البريد الإلكتروني", + "@nameOrEmailAddress": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "enterSearchTerm": "أدخل مصطلح البحث", + "@enterSearchTerm": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "clearFilter": "أزل الفلتر", + "@clearFilter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "advancedSearch": "البحث المتقدم", + "@advancedSearch": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "selectMailbox": "حدد صندوق البريد", + "@selectMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDuplicateTagFilterMail": "لقد أدخلت ذلك بالفعل", + "@messageDuplicateTagFilterMail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "chooseImage": "اختر صورة", + "@chooseImage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bulletedList": "قائمة نقطية", + "@bulletedList": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "addNewRule": "أضف القاعدة", + "@addNewRule": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "rulesNameHintTextInput": "أدخل اسم القاعدة", + "@rulesNameHintTextInput": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "exactlyEquals": "يساوي بالضبط", + "@exactlyEquals": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "errorMessageWhenMessageVacationIsEmpty": "لا يمكن أن يكون نص الرسالة فارغًا", + "@errorMessageWhenMessageVacationIsEmpty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteRecipient": "إزالة المستلمين", + "@deleteRecipient": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageConfirmationDialogDeleteAllRecipientForward": "هل أنت متأكد أنك تريد إزالة هؤلاء المستلمين؟ سيؤدي القيام بذلك إلى إزالتها من سلسلة البريد الإلكتروني.", + "@messageConfirmationDialogDeleteAllRecipientForward": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "addRecipients": "إضافة المستلمين", + "@addRecipients": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageDeleteEmailRuleSuccessfully": "تم إزالة القاعدة.", + "@toastMessageDeleteEmailRuleSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "requiredUrl": "عنوان الخادم مطلوب", + "@requiredUrl": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "jmapBasedMailSolution": "JMAP-based\ncollaborative team mail solution", + "@jmapBasedMailSolution": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "jmapStandard": "JMAP standard", + "@jmapStandard": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "manageEmailAsATeam": "إدارة البريد الإلكتروني كفريق", + "@manageEmailAsATeam": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "multipleIntegrations": "تكاملات متعددة", + "@multipleIntegrations": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "personalFolders": "المجلدات الشخصية", + "@personalFolders": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "new_folder": "مجلد جديد", + "@new_folder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "reply_all": "الرد على الجميع", + "@reply_all": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "reply": "الرد", + "@reply": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "forward": "إعادة توجيه", + "@forward": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "no_mail_selected": "لم يتم تحديد أي بريد إلكتروني", + "@no_mail_selected": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "from_email_address_prefix": "من", + "@from_email_address_prefix": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "to_email_address_prefix": "إلى", + "@to_email_address_prefix": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "unread_email_notification": "جديد", + "@unread_email_notification": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bcc_email_address_prefix": "Bcc", + "@bcc_email_address_prefix": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "cc_email_address_prefix": "Cc", + "@cc_email_address_prefix": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hint_text_email_address": "اسم أو عنوان البريد الإلكتروني", + "@hint_text_email_address": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subject_email": "الموضوع", + "@subject_email": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "header_email_quoted": "في{sentDate}، من {emailAddress}", + "@header_email_quoted": { + "type": "text", + "placeholders_order": [ + "sentDate", + "emailAddress" + ], + "placeholders": { + "sentDate": {}, + "emailAddress": {} + } + }, + "prefix_forward_email": ":Fwd", + "@prefix_forward_email": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "your_email_being_sent": "جاري إرسال رسالتك البريدية...", + "@your_email_being_sent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "message_dialog_send_email_without_recipient": "يجب أن يكون لديك على الأقل مستلم واحد لرسالتك البريدية.", + "@message_dialog_send_email_without_recipient": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "count_email_selected": "تم تحديد {count}", + "@count_email_selected": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "move_to_trash": "نقل إلى سلة المهملات", + "@move_to_trash": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "marked_multiple_item_as_unread": "تم وضع علامة كغير مقروء لـ {count} عنصر", + "@marked_multiple_item_as_unread": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "attachment_download_failed": "فشل تنزيل المرفق", + "@attachment_download_failed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "downloading_file": "جارٍ تنزيل {fileName}", + "@downloading_file": { + "type": "text", + "placeholders_order": [ + "fileName" + ], + "placeholders": { + "fileName": {} + } + }, + "preparing_to_export": "جارٍ التحضير للتصدير", + "@preparing_to_export": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "cancel": "إلغاء", + "@cancel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "you_need_to_grant_files_permission_to_download_attachments": "يجب عليك منح إذن لتنزيل المرفقات.", + "@you_need_to_grant_files_permission_to_download_attachments": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attach_file_prepare_text": "جارٍ التحضير لإرفاق الملف...", + "@attach_file_prepare_text": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attachments_uploaded_successfully": "تم تحميل المرفقات بنجاح.", + "@attachments_uploaded_successfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pick_attachments": "اختر المرفقات", + "@pick_attachments": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "photos_and_videos": "الصور ومقاطع الفيديو", + "@photos_and_videos": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "browse": "تصفح", + "@browse": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "moved_to_mailbox": "تم نقلها إلى {destinationMailboxPath}", + "@moved_to_mailbox": { + "type": "text", + "placeholders_order": [ + "destinationMailboxPath" + ], + "placeholders": { + "destinationMailboxPath": {} + } + }, + "undo": "تراجع", + "@undo": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "marked_star_multiple_item": "تم وضع علامة مفضلة على {count} عنصر", + "@marked_star_multiple_item": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "marked_unstar_multiple_item": "تم إزالة علامة المفضلة عن {count} عنصر", + "@marked_unstar_multiple_item": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "search_mail": "البحث في البريد الإلكتروني", + "@search_mail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "prefix_suggestion_search": "البحث عن", + "@prefix_suggestion_search": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "no_emails_matching_your_search": "لا توجد رسائل تطابق بحثك", + "@no_emails_matching_your_search": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "results": "النتائج", + "@results": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hint_search_emails": "البحث عن رسائل البريد الإلكتروني والملفات", + "@hint_search_emails": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "compose": "إنشاء رسالة", + "@compose": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delete": "حذف", + "@delete": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "move": "نقل", + "@move": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "flag": "وضع علامة", + "@flag": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "folders": "المجلدات", + "@folders": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sign_out": "تسجيل الخروج", + "@sign_out": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hint_search_mailboxes": "البحث في صناديق البريد", + "@hint_search_mailboxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "starred": "المفضلة", + "@starred": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "filter_message_toast": "لقد قمت بتصفية الرسائل حسب \"{filterOption}\".", + "@filter_message_toast": { + "type": "text", + "placeholders_order": [ + "filterOption" + ], + "placeholders": { + "filterOption": {} + } + }, + "with_unread": "مع غير المقروءة", + "@with_unread": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "with_starred": "مع المفضلة", + "@with_starred": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "message_has_been_sent_successfully": "تم إرسال الرسالة بنجاح", + "@message_has_been_sent_successfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "done": "تم", + "@done": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "new_mailbox": "صندوق بريد جديد", + "@new_mailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "name_of_mailbox_is_required": "اسم صندوق البريد مطلوب", + "@name_of_mailbox_is_required": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailbox_name_cannot_contain_special_characters": "اسم صندوق البريد لا يمكن أن يحتوي على أحرف خاصة", + "@mailbox_name_cannot_contain_special_characters": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "this_folder_name_is_already_taken": "اسم هذا المجلد مستخدم بالفعل", + "@this_folder_name_is_already_taken": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "new_mailbox_is_created": "تم إنشاء صندوق البريد {nameMailbox}", + "@new_mailbox_is_created": { + "type": "text", + "placeholders_order": [ + "nameMailbox" + ], + "placeholders": { + "nameMailbox": {} + } + }, + "create_new_mailbox_failure": "فشل إنشاء صندوق البريد الجديد", + "@create_new_mailbox_failure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "drafts_saved": "تم حفظ المسودة", + "@drafts_saved": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "discard": "تجاهل", + "@discard": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "skip": "تخطي", + "@skip": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hint_input_create_new_mailbox": "يرجى إدخال اسم صندوق البريد", + "@hint_input_create_new_mailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "rename": "إعادة تسمية", + "@rename": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delete_mailboxes_successfully": "تم حذف صناديق البريد بنجاح", + "@delete_mailboxes_successfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "there_is_already_folder_with_the_same_name": "يوجد بالفعل مجلد بنفس الاسم", + "@there_is_already_folder_with_the_same_name": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "email_address_is_not_in_the_correct_format": "عنوان البريد الإلكتروني غير مكتمل بالتنسيق الصحيح", + "@email_address_is_not_in_the_correct_format": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "preparing_to_save": "جارٍ التحضير للحفظ", + "@preparing_to_save": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "copy_email_address": "نسخ عنوان البريد الإلكتروني", + "@copy_email_address": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "compose_email": "إنشاء رسالة بريد إلكتروني", + "@compose_email": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "email_address_copied_to_clipboard": "تم نسخ عنوان البريد الإلكتروني إلى الحافظة", + "@email_address_copied_to_clipboard": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "fullscreen": "ملء الشاشة", + "@fullscreen": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "close": "إغلاق", + "@close": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "showAll": "عرض الكل", + "@showAll": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "message_dialog_send_email_without_a_subject": "هل أنت متأكد من إرسال الرسائل بدون موضوع؟", + "@message_dialog_send_email_without_a_subject": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "app_name": "Team Mail", + "@app_name": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "search_emails": "البحث في رسائل البريد الإلكتروني", + "@search_emails": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "select_all": "تحديد الكل", + "@select_all": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "not_starred": "غير المفضلة", + "@not_starred": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "select": "تحديد", + "@select": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "expand": "توسيع", + "@expand": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "collapse": "طي", + "@collapse": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "save_to_drafts": "حفظ في المسودة", + "@save_to_drafts": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hint_compose_email": "ابدأ في كتابة رسالة...", + "@hint_compose_email": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "fix_email_addresses": "قم بتصحيح عناوين البريد الإلكتروني", + "@fix_email_addresses": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "your_download_has_started": "بدأ تنزيل الملف", + "@your_download_has_started": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toast_message_delete_multiple_email_permanently_success": "تم حذف {count} رسالة بشكل دائم", + "@toast_message_delete_multiple_email_permanently_success": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "toast_message_delete_a_email_permanently_success": "تم حذف الرسالة بشكل دائم", + "@toast_message_delete_a_email_permanently_success": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delete_permanently": "حذف بشكل دائم", + "@delete_permanently": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delete_messages_forever": "حذف الرسائل بشكل دائم", + "@delete_messages_forever": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toast_message_empty_trash_folder_success": "تم حذف جميع الرسائل بشكل دائم", + "@toast_message_empty_trash_folder_success": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "version": "الإصدار", + "@version": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "un_star": "إزالة العلامة النجمية", + "@un_star": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "un_spam": "إزالة العلامة كرسالة غير مرغوب فيها (سبام)", + "@un_spam": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "settings": "الإعدادات", + "@settings": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "manage_account": "إدارة الحساب", + "@manage_account": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "profiles": "الملفات الشخصية", + "@profiles": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "profilesSettingExplanation": "معلومات عنك وخيارات لإدارتها.", + "@profilesSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "identities": "المتطابقات", + "@identities": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "setDefaultIdentity": "تعيين كهوية افتراضية", + "@setDefaultIdentity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "identitiesSettingExplanation": "حدد الهوية أو عنوان البريد الإلكتروني الذي تريد استخدامه لإرسال بريد إلكتروني", + "@identitiesSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "createNewIdentity": "خلق هوية جديدة", + "@createNewIdentity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "new_identity": "هوية جديدة", + "@new_identity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "name": "الاسم", + "@name": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "html_template": "Html template", + "@html_template": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "create": "إنشاء", + "@create": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "you_have_created_a_new_identity": "لقد قمت بإنشاء هوية جديدة", + "@you_have_created_a_new_identity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "default_value": "الافتراضية", + "@default_value": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delete_identity": "حذف الهوية", + "@delete_identity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "message_confirmation_dialog_delete_identity": "هل أنت متأكد أنك تريد حذف هذه الهوية؟", + "@message_confirmation_dialog_delete_identity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "identity_has_been_deleted": "تم حذف الهوية", + "@identity_has_been_deleted": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "edit_identity": "تحرير الهوية", + "@edit_identity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "you_are_changed_your_identity_successfully": "لقد قمت بتغيير هويتك بنجاح", + "@you_are_changed_your_identity_successfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "save": "حفظ", + "@save": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hasAttachment": "لديها مرفق", + "@hasAttachment": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "last7Days": "اخر 7 ايام", + "@last7Days": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "showingResultsFor": "عرض النتائج ل:", + "@showingResultsFor": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "last30Days": "آخر 30 يومًا", + "@last30Days": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "thisEmailAddressInvalid": "عنوان البريد الإلكتروني هذا غير صالح", + "@thisEmailAddressInvalid": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "loginInputSSOMessage": "تسجيل الدخول بحساب SSO الخاص بي", + "@loginInputSSOMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "canNotVerifySSOConfiguration": "لا يمكن التحقق من تكوين SSO ، يرجى مراجعة مسؤول النظام", + "@canNotVerifySSOConfiguration": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "canNotGetToken": "لا يمكن الحصول على رمز ، يرجى مراجعة مسؤول النظام الخاص بك", + "@canNotGetToken": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "moveMailbox": "نقل صندوق البريد", + "@moveMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageMarkAsMailboxReadSuccess": "لقد وضعت علامة على جميع الرسائل في \"{mailboxName}\" كمقروءة", + "@toastMessageMarkAsMailboxReadSuccess": { + "type": "text", + "placeholders_order": [ + "mailboxName" + ], + "placeholders": { + "mailboxName": {} + } + }, + "allMailboxes": "كل علب البريد", + "@allMailboxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "singleSignOn": "علامة واحدة على", + "@singleSignOn": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "ssoNotAvailable": "تسجيل الدخول الأحادي (SSO) غير متاح", + "@ssoNotAvailable": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "form": "من", + "@form": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subject": "الموضوع", + "@subject": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "to": "إلى", + "@to": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hasTheWords": "لديه الكلمات", + "@hasTheWords": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "doesNotHave": "لا يملك", + "@doesNotHave": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailbox": "صندوق بريد", + "@mailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageAndRegion": "اللغة و المنطقة", + "@languageAndRegion": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageAndRegionSubtitle": "قم بتعيين اللغة والمنطقة الزمنية وتنسيق الوقت الذي تستخدمه في TeamMail.", + "@languageAndRegionSubtitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "insertImage": "إدراج صورة", + "@insertImage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "selectFromFile": "اختر من ملف", + "@selectFromFile": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "language": "اللغة", + "@language": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageEnglish": "اللغة الإنجليزية", + "@languageEnglish": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageVietnamese": "اللغة الفيتنامية", + "@languageVietnamese": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageFrench": "اللغة الفرنسية", + "@languageFrench": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageRussian": "اللغة الروسية", + "@languageRussian": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogSendEmailUploadingAttachment": "تعذر إرسال رسالتك لأنها تقوم بتحميل المرفق", + "@messageDialogSendEmailUploadingAttachment": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "alignRight": "محاذاة إلى اليمين", + "@alignRight": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "paragraph": "فقرة", + "@paragraph": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "justifyFull": "ضبط كامل", + "@justifyFull": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "outdent": "إزالة المسافة البادئة", + "@outdent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "alignLeft": "محاذاة إلى اليسار", + "@alignLeft": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "alignCenter": "محاذاة في الوسط", + "@alignCenter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacationSettingSaved": "تم حفظ إعدادات الإجازة", + "@vacationSettingSaved": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "yourVacationResponderIsEnabled": "تم تمكين المجيب التلقائي الخاص بك.", + "@yourVacationResponderIsEnabled": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "yourVacationResponderIsDisabledSuccessfully": "تم تعطيل المجيب التلقائي بنجاح", + "@yourVacationResponderIsDisabledSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "endNow": "تنتهي الآن", + "@endNow": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacationSetting": "إعداد الإجازة", + "@vacationSetting": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "backToSearchResults": "رجوع إلى نتائج البحث", + "@backToSearchResults": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hintSearchInputContact": "أدخل الاسم أو البريد الإلكتروني", + "@hintSearchInputContact": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "format": "شكل", + "@format": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "background": "الخلفية", + "@background": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "foreground": "المقدمة", + "@foreground": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleFormat": "شكل", + "@titleFormat": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleQuickStyles": "الأساليب السريعة", + "@titleQuickStyles": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleBackground": "الخلفية", + "@titleBackground": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "setDate": "حدد التاريخ", + "@setDate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageErrorWhenSelectStartDateIsEmpty": "لا يمكن أن يكون تاريخ البدء فارغًا.", + "@toastMessageErrorWhenSelectStartDateIsEmpty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "selectDate": "حدد تاريخ", + "@selectDate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageErrorWhenSelectEndDateIsEmpty": "لا يمكن أن يكون تاريخ الانتهاء فارغًا.", + "@toastMessageErrorWhenSelectEndDateIsEmpty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "customRange": "نطاق مخصص", + "@customRange": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "selectParentFolder": "حدد المجلد الأصل", + "@selectParentFolder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "requestReadReceipt": "طلب إيصال بالقراءة", + "@requestReadReceipt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textBodySendReceiptToSender": "تمت قراءة الرسالة بواسطة {Receiver} في {time}\n\nالموضوع: {subject}\n\nملاحظة: يقر إيصال إعادة القراءة هذا فقط بأنه تم عرض الرسالة على كمبيوتر المستلم. ليس هناك ما يضمن أن المستلم قد قرأ أو فهم محتويات الرسالة.", + "@textBodySendReceiptToSender": { + "type": "text", + "placeholders_order": [ + "receiver", + "subject", + "time" + ], + "placeholders": { + "receiver": {}, + "subject": {}, + "time": {} + } + }, + "toastMessageErrorNotSelectedFolderWhenCreateNewMailbox": "لم تحدد مجلد حفظ لحفظه", + "@toastMessageErrorNotSelectedFolderWhenCreateNewMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "moveConversation": "انقل محادثة {numberOfConversation}", + "@moveConversation": { + "type": "text", + "placeholders_order": [ + "numberOfConversation" + ], + "placeholders": { + "numberOfConversation": {} + } + }, + "addRecipientButton": "أضف المستلم", + "@addRecipientButton": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "totalEmailSelected": "إلغاء تحديد الكل ({count})", + "@totalEmailSelected": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "newer": "الأحدث", + "@newer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "older": "الأقدم", + "@older": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "forwardingSettingExplanation": "يسمح للمستلم الجديد برؤية البريد الإلكتروني المرسل إذا لم يتم تضمينه في الأصل في سلسلة البريد الإلكتروني.", + "@forwardingSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "storageQuotas": "التخزين", + "@storageQuotas": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasUsed": "{used} GB من {softLimit} GB مستخدمة", + "@textQuotasUsed": { + "type": "text", + "placeholders_order": [ + "used", + "softLimit" + ], + "placeholders": { + "used": {}, + "softLimit": {} + } + }, + "textQuotasRunningOutOfStorageTitle": "سعة التخزين على وشك النفاد ({progress}٪).", + "@textQuotasRunningOutOfStorageTitle": { + "type": "text", + "placeholders_order": [ + "progress" + ], + "placeholders": { + "progress": {} + } + }, + "textQuotasRunOutOfStorageTitle": "لقد نفدت مساحة التخزين لديك", + "@textQuotasRunOutOfStorageTitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasRunOutOfStorageContent": "الآن لا يمكنك إرسال بريد إلكتروني أو تلقيه مؤقتًا. يرجى تفريغ أو ترقية مساحة التخزين الخاصة بك للحصول على الميزات الكاملة لبريد الفريق.", + "@textQuotasRunOutOfStorageContent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quickCreatingRule": "إنشاء قاعدة مع هذا البريد الإلكتروني", + "@quickCreatingRule": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titlePageNotFound": "عفوًا ، لا يمكننا العثور على تلك الصفحة", + "@titlePageNotFound": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "countNewSpamEmails": "لديك {count} رسائل بريد إلكتروني جديدة غير مرغوب فيها!", + "@countNewSpamEmails": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "dismiss": "رفض", + "@dismiss": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "disableSpamReport": "تعطيل تقرير البريد العشوائي", + "@disableSpamReport": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "enableSpamReport": "تمكين تقرير البريد العشوائي", + "@enableSpamReport": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailureWithSetErrorTypeTooLarge": "فشل في إرسال رسالتك لأنها كبيرة جدًا.", + "@sendMessageFailureWithSetErrorTypeTooLarge": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailureWithSetErrorTypeOverQuota": "عدم إرسال رسالتك ، لأنها تجاوزت الحصة المحددة.", + "@sendMessageFailureWithSetErrorTypeOverQuota": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailure": "فشل في حفظ رسالتك كمسودات.", + "@saveEmailAsDraftFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailureWithSetErrorTypeTooLarge": "فشل في حفظ رسالتك كمسودات ، لأنها كبيرة جدًا.", + "@saveEmailAsDraftFailureWithSetErrorTypeTooLarge": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailureWithSetErrorTypeOverQuota": "فشل في حفظ رسالتك كمسودات ، لأنها تجاوزت الحصة.", + "@saveEmailAsDraftFailureWithSetErrorTypeOverQuota": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "teamMailBoxes": "Team-mailboxes", + "@teamMailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMsgHideMailboxSuccess": "تم إخفاء صندوق البريد هذا من صندوق البريد الأساسي الخاص بك", + "@toastMsgHideMailboxSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "searchForMailboxes": "ابحث عن علب البريد", + "@searchForMailboxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasOutOfStorage": "نفاد التخزين", + "@textQuotasOutOfStorage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "appTitlePushNotification": "Team Mail", + "@appTitlePushNotification": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hideMailBoxes": "إخفاء صندوق البريد", + "@hideMailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "showMailbox": "إظهار علبة البريد", + "@showMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageShowMailboxSuccess": "صندوق البريد هذا معروض بالفعل في صندوق البريد الأساسي الخاص بك", + "@toastMessageShowMailboxSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "no": "لا", + "@no": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageCannotFoundIdentityWhenSendReceipt": "لا يمكن العثور على معرف الهوية المعطى", + "@toastMessageCannotFoundIdentityWhenSendReceipt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleForeground": "المقدمة", + "@titleForeground": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageErrorWhenSelectDateIsInValid": "لا يمكن أن يكون وقت الانتهاء أقل من وقت البدء.", + "@toastMessageErrorWhenSelectDateIsInValid": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateRangeAdvancedSearchFilter": "من {startDate} إلى {endDate}", + "@dateRangeAdvancedSearchFilter": { + "type": "text", + "placeholders_order": [ + "startDate", + "endDate" + ], + "placeholders": { + "startDate": {}, + "endDate": {} + } + }, + "appGridTittle": "انتقل إلى التطبيقات", + "@appGridTittle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleReadReceiptRequestNotificationMessage": "قراءة طلب إيصال", + "@titleReadReceiptRequestNotificationMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "yes": "نعم", + "@yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "incorrectEmailFormat": "صيغة بريد إلكتروني غير صحيحة", + "@incorrectEmailFormat": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "remove": "إزالة", + "@remove": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailboxVisibility": "رؤية صندوق البريد", + "@mailboxVisibility": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasRunningOutOfStorageContent": "قريبًا لن تتمكن من إرسال بريد إلكتروني في Team Mail. الرجاء تنظيف مساحة التخزين الخاصة بك أو ترقية مساحة التخزين لديك للحصول على الميزات الكاملة في بريد الفريق.", + "@textQuotasRunningOutOfStorageContent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "regards": "يعتبر", + "@regards": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "privacyPolicy": "سياسة الخصوصية", + "@privacyPolicy": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "reduceSomeFiltersAndTryAgain": "لنقم بتقليل بعض الفلاتر وحاول مرة أخرى", + "@reduceSomeFiltersAndTryAgain": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youHaveNewMessages": "لديك رسائل جديدة", + "@youHaveNewMessages": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEmailInYourCurrentMailbox": "معذرةً ، لا توجد رسائل بريد إلكتروني في صندوق بريدك الحالي", + "@noEmailInYourCurrentMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailBoxes": "علب البريد", + "@mailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "thisImageCannotBeAdded": "لا يمكن إضافة هذه الصورة.", + "@thisImageCannotBeAdded": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailboxVisibilitySubtitle": "إظهار / إخفاء علب البريد الخاصة بك ، بما في ذلك علب البريد الشخصية والفريق.", + "@mailboxVisibilitySubtitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyListEmailForward": "الرجاء إدخال مستلم واحد على الأقل", + "@emptyListEmailForward": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "clearAll": "امسح الكل", + "@clearAll": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "contact": "اتصال", + "@contact": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quickStyles": "الأساليب السريعة", + "@quickStyles": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subTitleReadReceiptRequestNotificationMessage": "لقد طلب المرسل إيصالاً بالقراءة لهذا البريد الإلكتروني. إرسال إيصال بالقراءة؟", + "@subTitleReadReceiptRequestNotificationMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageNotSupportMdnWhenSendReceipt": "حسابك لا يدعم إمكانية MDN", + "@toastMessageNotSupportMdnWhenSendReceipt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageSendReceiptSuccess": "تم إرسال إيصال بالقراءة.", + "@toastMessageSendReceiptSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageCannotFoundEmailIdWhenSendReceipt": "لا يمكن العثور على معرف البريد الإلكتروني المعطى", + "@toastMessageCannotFoundEmailIdWhenSendReceipt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subjectSendReceiptToSender": "قراءة: {subject}", + "@subjectSendReceiptToSender": { + "type": "text", + "placeholders_order": [ + "subject" + ], + "placeholders": { + "subject": {} + } + }, + "createNewMailbox": "إنشاء صندوق بريد جديد", + "@createNewMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageConfirmationDialogDeleteMultipleMailbox": "سيتم حذف صندوق بريد {numberOfMailbox} وجميع المجلدات الفرعية والرسائل التي يحتوي عليها ولن تتمكن من استعادتها. هل تريد الاستمرار في الحذف؟", + "@messageConfirmationDialogDeleteMultipleMailbox": { + "type": "text", + "placeholders_order": [ + "numberOfMailbox" + ], + "placeholders": { + "numberOfMailbox": {} + } + }, + "page404": "صفحة 404", + "@page404": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "totalNewMessagePushNotification": "{count} رسائل بريد إلكتروني جديدة", + "@totalNewMessagePushNotification": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "showDetails": "اظهر التفاصيل", + "@showDetails": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subTitlePageNotFound": "من الممكن أن تكون صفحة الوجهة الخاصة بك قد اختفت أو تنتمي إلى حساب آخر.", + "@subTitlePageNotFound": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "openInNewTab": "فتح في علامة تبويب جديدة", + "@openInNewTab": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "required": "مطلوب", + "@required": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEmailMatchYourCurrentFilter": "معذرةً ، لا توجد رسائل بريد إلكتروني تطابق عامل التصفية الحالي.", + "@noEmailMatchYourCurrentFilter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailure": "فشل في ارسال رسالتك.", + "@sendMessageFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "spamMailboxDisplayName": "البريد غير الهام", + "@spamMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "draftsMailboxDisplayName": "مسودات", + "@draftsMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "archiveMailboxDisplayName": "أرشيف", + "@archiveMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "inboxMailboxDisplayName": "الوارد", + "@inboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sentMailboxDisplayName": "المرسل", + "@sentMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "outboxMailboxDisplayName": "الصادر", + "@outboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "trashMailboxDisplayName": "المهملات", + "@trashMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "templatesMailboxDisplayName": "القوالب", + "@templatesMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + } +} diff --git a/lib/l10n/intl_ca.arb b/lib/l10n/intl_ca.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_ca.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_de.arb b/lib/l10n/intl_de.arb index 882444d577..60c4b2a4c6 100644 --- a/lib/l10n/intl_de.arb +++ b/lib/l10n/intl_de.arb @@ -11,7 +11,7 @@ "placeholders_order": [], "placeholders": {} }, - "initializing_data": "Daten werden initialisiert …", + "initializing_data": "Daten werden initialisiert...", "@initializing_data": { "type": "text", "placeholders_order": [], diff --git a/lib/l10n/intl_et.arb b/lib/l10n/intl_et.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_et.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_fr.arb b/lib/l10n/intl_fr.arb index aaa1bdafe8..16f07545f2 100644 --- a/lib/l10n/intl_fr.arb +++ b/lib/l10n/intl_fr.arb @@ -1,6 +1,6 @@ { "@@last_modified": "2021-10-13T22:37:55.795262", - "initializing_data": "Initialisation des données…", + "initializing_data": "Initialisation des données...", "@initializing_data": { "type": "text", "placeholders_order": [], @@ -1040,7 +1040,7 @@ "placeholders_order": [], "placeholders": {} }, - "delete_multiple_messages_dialog": "Vous êtes sur le point de supprimer définitivement {count} éléments de la Corbeille. Voulez-vous continuer ?", + "delete_multiple_messages_dialog": "Vous êtes sur le point de supprimer définitivement {count} éléments dans {mailboxName}. Voulez-vous continuer ?", "@delete_multiple_messages_dialog": { "type": "text", "placeholders_order": [ @@ -1891,5 +1891,1467 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "personalFolders": "Dossiers personnels", + "@personalFolders": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "profilesSettingExplanation": "Informations vous concernant et options pour les gérer.", + "@profilesSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "setDefaultIdentity": "Définir comme identité par défaut", + "@setDefaultIdentity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "identitiesSettingExplanation": "Sélectionnez l'identité ou l'adresse e-mail que vous souhaitez utiliser pour envoyer un e-mail", + "@identitiesSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "createNewIdentity": "Créer une nouvelle identité", + "@createNewIdentity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bcc_to": "Cci à", + "@bcc_to": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "html": "Html", + "@html": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "you_have_created_a_new_default_identity": "Vous avez créé une nouvelle identité par défaut", + "@you_have_created_a_new_default_identity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hintInputAutocompleteContact": "Entrez le nom ou l'adresse e-mail", + "@hintInputAutocompleteContact": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageConfirmationDialogDeleteEmailRule": "Voulez-vous supprimer la règle \"{ruleName}\" ?", + "@messageConfirmationDialogDeleteEmailRule": { + "type": "text", + "placeholders_order": [ + "ruleName" + ], + "placeholders": { + "ruleName": {} + } + }, + "deleteEmailRule": "Supprimer la règle", + "@deleteEmailRule": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleBackground": "Arrière-plan", + "@titleBackground": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "requestReadReceipt": "Demander un accusé de lecture", + "@requestReadReceipt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "appGridTittle": "Accéder aux applications", + "@appGridTittle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleReadReceiptRequestNotificationMessage": "Demander un accusé de réception", + "@titleReadReceiptRequestNotificationMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageNotSupportMdnWhenSendReceipt": "Votre compte ne prend pas en charge la fonctionnalité MDN", + "@toastMessageNotSupportMdnWhenSendReceipt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "newer": "Plus récent", + "@newer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "older": "Plus ancien", + "@older": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "regards": "Salutations", + "@regards": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youHaveNewMessages": "Vous avez de nouveaux messages", + "@youHaveNewMessages": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "appTitlePushNotification": "Team Mail", + "@appTitlePushNotification": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEmailMatchYourCurrentFilter": "Nous sommes désolés, aucun e-mail ne correspond à votre filtre actuel.", + "@noEmailMatchYourCurrentFilter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailureWithSetErrorTypeOverQuota": "Échec de l'envoi de votre message, car vous dépassez votre quota.", + "@sendMessageFailureWithSetErrorTypeOverQuota": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailBoxes": "Boîtes aux lettres", + "@mailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "searchForMailboxes": "Rechercher des boîtes aux lettres", + "@searchForMailboxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "showMailbox": "Afficher la boîte aux lettres", + "@showMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageShowMailboxSuccess": "Cette boîte aux lettres est déjà affichée dans votre boîte aux lettres principale", + "@toastMessageShowMailboxSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "actionTitleRulesFilter": "Effectuez l'action suivante :", + "@actionTitleRulesFilter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "conditionTitleRulesFilter": "Si toutes les conditions suivantes sont remplies :", + "@conditionTitleRulesFilter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "headerRecipients": "Destinataires", + "@headerRecipients": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "forwarding": "Adresse de réexpédition", + "@forwarding": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "startDate": "Date de début", + "@startDate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "endDate": "Date de fin", + "@endDate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "message": "Message", + "@message": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noStartTime": "Pas d'heure de début", + "@noStartTime": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEndTime": "Pas de date de fin", + "@noEndTime": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEndDate": "Pas de date de fin", + "@noEndDate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "errorMessageWhenStartDateVacationIsEmpty": "Veuillez entrer une date de début valide", + "@errorMessageWhenStartDateVacationIsEmpty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "errorMessageWhenEndDateVacationIsInValid": "La date de fin doit être supérieure à la date de début", + "@errorMessageWhenEndDateVacationIsInValid": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "errorMessageWhenMessageVacationIsEmpty": "Le corps du message ne peut pas être vide", + "@errorMessageWhenMessageVacationIsEmpty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacationSettingSaved": "Paramètres de vacances enregistrés", + "@vacationSettingSaved": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastErrorMessageWhenCreateNewRule": "Vous n'avez pas complètement rempli les informations.", + "@toastErrorMessageWhenCreateNewRule": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacationSettingExplanation": "Envoie une réponse automatique aux messages entrants.", + "@vacationSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacationSettingToggleButtonAutoReply": "Répondez automatiquement aux messages lorsqu'ils sont reçus.", + "@vacationSettingToggleButtonAutoReply": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "startTime": "Heure de début", + "@startTime": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hintSubjectInputVacationSetting": "Entrez le sujet", + "@hintSubjectInputVacationSetting": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "backToSearchResults": "Retour aux résultats de recherche", + "@backToSearchResults": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "clearAll": "Tout effacer", + "@clearAll": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "customRange": "Plage temporelle personnalisée", + "@customRange": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "selectParentFolder": "Sélectionnez le dossier parent", + "@selectParentFolder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "contact": "Contact", + "@contact": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "yes": "Oui", + "@yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "no": "Non", + "@no": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageCannotFoundEmailIdWhenSendReceipt": "L'identifiant e-mail indiquée est introuvable", + "@toastMessageCannotFoundEmailIdWhenSendReceipt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subjectSendReceiptToSender": "Lire : {subject}", + "@subjectSendReceiptToSender": { + "type": "text", + "placeholders_order": [ + "subject" + ], + "placeholders": { + "subject": {} + } + }, + "messageConfirmationDialogDeleteMultipleMailbox": "La boîte aux lettres {numberOfMailbox} et tous les sous-dossiers et messages qu'elle contient seront supprimés et ne pourront pas être récupérés. Voulez-vous continuer la suppression ?", + "@messageConfirmationDialogDeleteMultipleMailbox": { + "type": "text", + "placeholders_order": [ + "numberOfMailbox" + ], + "placeholders": { + "numberOfMailbox": {} + } + }, + "toastMessageErrorNotSelectedFolderWhenCreateNewMailbox": "Vous n'avez pas sélectionné de dossier parent pour enregistrer pour enregistrer cette boîte aux lettres", + "@toastMessageErrorNotSelectedFolderWhenCreateNewMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "addRecipientButton": "Ajouter un destinataire", + "@addRecipientButton": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "incorrectEmailFormat": "Format d'e-mail incorrect", + "@incorrectEmailFormat": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "remove": "Retirer", + "@remove": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "totalEmailSelected": "Tout désélectionner ({count})", + "@totalEmailSelected": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "storageQuotas": "Stockage", + "@storageQuotas": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasUsed": "{used} Go sur {softLimit} Go utilisés", + "@textQuotasUsed": { + "type": "text", + "placeholders_order": [ + "used", + "softLimit" + ], + "placeholders": { + "used": {}, + "softLimit": {} + } + }, + "textQuotasRunningOutOfStorageTitle": "Vous manquez d'espace de stockage ({progress} %).", + "@textQuotasRunningOutOfStorageTitle": { + "type": "text", + "placeholders_order": [ + "progress" + ], + "placeholders": { + "progress": {} + } + }, + "textQuotasOutOfStorage": "Stockage épuisé", + "@textQuotasOutOfStorage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasRunOutOfStorageContent": "Maintenant, vous ne pouvez temporairement pas envoyer ou recevoir d'e-mail. Veuillez libérer ou mettre à niveau votre espace de stockage pour bénéficier de toutes les fonctionnalités de Team Mail.", + "@textQuotasRunOutOfStorageContent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quickCreatingRule": "Créer une règle à partir de cet e-mail", + "@quickCreatingRule": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleHeaderAttachment": "{count} Pièces jointes ({totalSize}) :", + "@titleHeaderAttachment": { + "type": "text", + "placeholders_order": [ + "count", + "totalSize" + ], + "placeholders": { + "count": {}, + "totalSize": {} + } + }, + "undo": "Annuler", + "@undo": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "skip": "Ignorer", + "@skip": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "exactlyEquals": "Exactement égal", + "@exactlyEquals": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "insertImageErrorDuplicate": "Veuillez saisir une image ou une URL d'image, pas les deux", + "@insertImageErrorDuplicate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "outdent": "Retrait extérieur", + "@outdent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "indent": "Retrait", + "@indent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "reply_to": "Répondre à", + "@reply_to": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "showAll": "Tout montrer", + "@showAll": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageDeleteEmailRuleSuccessfully": "La règle a été supprimée.", + "@toastMessageDeleteEmailRuleSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacationSetting": "Paramètres des vacances", + "@vacationSetting": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "endTime": "Heure de fin", + "@endTime": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveChanges": "Sauvegarder les modifications", + "@saveChanges": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageIsRequired": "Un message est requis", + "@messageIsRequired": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "endNow": "Termine maintenant", + "@endNow": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subTitleReadReceiptRequestNotificationMessage": "L'expéditeur a demandé l'envoi d'un accusé de réception pour cet e-mail. Envoyer un accusé de réception ?", + "@subTitleReadReceiptRequestNotificationMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageCannotFoundIdentityWhenSendReceipt": "L'identifiant d'identité fourni est introuvable", + "@toastMessageCannotFoundIdentityWhenSendReceipt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasRunningOutOfStorageContent": "Bientôt, vous ne pourrez plus envoyer d'e-mails via Team Mail. Veuillez nettoyer votre stockage ou mettre à niveau votre stockage pour obtenir toutes les fonctionnalités de Team Mail.", + "@textQuotasRunningOutOfStorageContent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "forwardingSettingExplanation": "Permet à un nouveau destinataire de voir l'e-mail envoyé s'il n'était pas initialement inclus dans la chaîne d'e-mails.", + "@forwardingSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "privacyPolicy": "Politique de confidentialité", + "@privacyPolicy": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "page404": "Page 404", + "@page404": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titlePageNotFound": "Oups, la page est introuvable", + "@titlePageNotFound": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "enableSpamReport": "Activer le rapport de spam", + "@enableSpamReport": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "required": "requis", + "@required": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasRunOutOfStorageTitle": "Vous n'avez plus d'espace de stockage", + "@textQuotasRunOutOfStorageTitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subTitlePageNotFound": "Il est possible que votre page de destination ait disparu ou appartienne à un autre compte.", + "@subTitlePageNotFound": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "openInNewTab": "Ouvrir dans un nouvel onglet", + "@openInNewTab": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "showDetails": "Afficher les détails", + "@showDetails": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "totalNewMessagePushNotification": "{count} nouveaux e-mails", + "@totalNewMessagePushNotification": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "countNewSpamEmails": "Vous avez {count} nouveaux spams !", + "@countNewSpamEmails": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "dismiss": "Rejeter", + "@dismiss": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "disableSpamReport": "Désactiver le rapport de spam", + "@disableSpamReport": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEmailInYourCurrentMailbox": "Nous sommes désolés, il n'y a pas d'e-mails dans votre boîte aux lettres actuelle", + "@noEmailInYourCurrentMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "reduceSomeFiltersAndTryAgain": "Retirez quelques filtres et réessayez", + "@reduceSomeFiltersAndTryAgain": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailure": "Impossible d'envoyer votre message.", + "@sendMessageFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailureWithSetErrorTypeTooLarge": "Échec de l'envoi de votre message, car il est trop volumineux.", + "@sendMessageFailureWithSetErrorTypeTooLarge": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailure": "Échec de l'enregistrement de votre message en tant que brouillons.", + "@saveEmailAsDraftFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailureWithSetErrorTypeTooLarge": "Échec de l'enregistrement de votre message en tant que brouillons, car il est trop volumineux.", + "@saveEmailAsDraftFailureWithSetErrorTypeTooLarge": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "thisImageCannotBeAdded": "Cette image ne peut pas être ajoutée.", + "@thisImageCannotBeAdded": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailureWithSetErrorTypeOverQuota": "Échec de l'enregistrement de votre message en tant que brouillons, car vous dépassez votre quota.", + "@saveEmailAsDraftFailureWithSetErrorTypeOverQuota": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hideMailBoxes": "Masquer la boîte aux lettres", + "@hideMailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMsgHideMailboxSuccess": "Cette boîte aux lettres a été masquée", + "@toastMsgHideMailboxSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "teamMailBoxes": "Boîtes aux lettres d'équipe", + "@teamMailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailboxVisibilitySubtitle": "Afficher/masquer vos boîtes aux lettres, y compris vos boîtes aux lettres personnelles et d'équipe.", + "@mailboxVisibilitySubtitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailboxVisibility": "Visibilité des boîtes aux lettres", + "@mailboxVisibility": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "notExactlyEquals": "Pas exactement égal", + "@notExactlyEquals": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toMailbox": "Vers la boîte aux lettres :", + "@toMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacation": "Vacances", + "@vacation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "activated": "Activé", + "@activated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deactivated": "Désactivé", + "@deactivated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "vacationStopsAt": "Les vacances s'arrêtent le", + "@vacationStopsAt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hintMessageBodyVacation": "Message", + "@hintMessageBodyVacation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "yourVacationResponderIsEnabled": "Votre répondeur de vacances est activé.", + "@yourVacationResponderIsEnabled": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "yourVacationResponderIsDisabledSuccessfully": "Votre répondeur de vacances a été désactivé avec succès", + "@yourVacationResponderIsDisabledSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEnableVacationResponderAutomatically": "Votre répondeur de vacances sera activé le {startDate}", + "@messageEnableVacationResponderAutomatically": { + "type": "text", + "placeholders_order": [ + "startDate" + ], + "placeholders": { + "startDate": {} + } + }, + "messageDisableVacationResponderAutomatically": "Votre répondeur de vacances s'est arrêté le {endDate}", + "@messageDisableVacationResponderAutomatically": { + "type": "text", + "placeholders_order": [ + "endDate" + ], + "placeholders": { + "endDate": {} + } + }, + "messageConfirmationDialogDeleteRecipientForward": "Voulez-vous supprimer l'e-mail {emailAddress} ?", + "@messageConfirmationDialogDeleteRecipientForward": { + "type": "text", + "placeholders_order": [ + "emailAddress" + ], + "placeholders": { + "emailAddress": {} + } + }, + "deleteRecipient": "Supprimer des destinataires", + "@deleteRecipient": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageDeleteRecipientSuccessfully": "L'e-mail a été supprimé de la liste des destinataires.", + "@toastMessageDeleteRecipientSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageConfirmationDialogDeleteAllRecipientForward": "Voulez-vous vraiment supprimer ces destinataires ? Cela les supprimera de la chaîne de messagerie.", + "@messageConfirmationDialogDeleteAllRecipientForward": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "addRecipients": "Ajouter des destinataires", + "@addRecipients": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageAddRecipientsSuccessfully": "Les e-mails ont été ajoutés à partir de la liste des destinataires.", + "@toastMessageAddRecipientsSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageLocalCopyEnable": "Gardez la copie locale activée.", + "@toastMessageLocalCopyEnable": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageLocalCopyDisable": "Gardez la copie locale désactivée.", + "@toastMessageLocalCopyDisable": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "keepLocalCopyForwardLabel": "Conservez une copie de l'e-mail dans la boîte de réception", + "@keepLocalCopyForwardLabel": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emailRuleSettingExplanation": "Création de règles pour gérer les messages entrants. Vous choisissez à la fois la condition qui déclenche une règle et les actions que la règle appliquera.", + "@emailRuleSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hintSearchInputContact": "Entrez le nom ou l'e-mail", + "@hintSearchInputContact": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quickStyles": "Styles rapides", + "@quickStyles": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "format": "Format", + "@format": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "background": "Arrière-plan", + "@background": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleFormat": "Format", + "@titleFormat": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleQuickStyles": "Styles rapides", + "@titleQuickStyles": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "foreground": "Premier plan", + "@foreground": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleForeground": "Premier plan", + "@titleForeground": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "selectDate": "Sélectionner une date", + "@selectDate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageErrorWhenSelectStartDateIsEmpty": "La date de début ne peut pas être nulle.", + "@toastMessageErrorWhenSelectStartDateIsEmpty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageErrorWhenSelectEndDateIsEmpty": "La date de fin ne peut pas être nulle.", + "@toastMessageErrorWhenSelectEndDateIsEmpty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageErrorWhenSelectDateIsInValid": "L'heure de fin ne peut pas être inférieure à l'heure de début.", + "@toastMessageErrorWhenSelectDateIsInValid": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "setDate": "Sélectionner une date", + "@setDate": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dateRangeAdvancedSearchFilter": "Du {startDate} au {endDate}", + "@dateRangeAdvancedSearchFilter": { + "type": "text", + "placeholders_order": [ + "startDate", + "endDate" + ], + "placeholders": { + "startDate": {}, + "endDate": {} + } + }, + "unknownError": "Une erreur inconnue s'est produite, veuillez réessayer", + "@unknownError": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textBodySendReceiptToSender": "Le message a été lu par {receiver} le {time}\n\nSujet : {subject}\n\nRemarque : Cet accusé de réception de lecture confirme uniquement que le message a été affiché sur l'ordinateur du destinataire. Il n'y a aucune garantie que le destinataire ait lu ou compris le contenu du message.", + "@textBodySendReceiptToSender": { + "type": "text", + "placeholders_order": [ + "receiver", + "subject", + "time" + ], + "placeholders": { + "receiver": {}, + "subject": {}, + "time": {} + } + }, + "toastMessageSendReceiptSuccess": "Un accusé de réception a été envoyé.", + "@toastMessageSendReceiptSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "moveConversation": "Déplacer {numberOfConversation} conversations", + "@moveConversation": { + "type": "text", + "placeholders_order": [ + "numberOfConversation" + ], + "placeholders": { + "numberOfConversation": {} + } + }, + "createNewMailbox": "Créer une nouvelle boîte aux lettres", + "@createNewMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageArabic": "Arabe", + "@languageArabic": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageItalian": "Italien", + "@languageItalian": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyListEmailForward": "Français", + "@emptyListEmailForward": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Bad credentials": "Mauvais identifiants", + "@Bad credentials": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sentMailboxDisplayName": "Envoyés", + "@sentMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "inboxMailboxDisplayName": "Boîte de réception", + "@inboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogDeleteSendingEmail": "La suppression d'un message hors ligne effacera définitivement son contenu. Vous ne pourrez pas annuler cette action ni récupérer le message de la corbeille.", + "@messageDialogDeleteSendingEmail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHaveBeenDeletedSuccessfully": "Les messages ont été supprimés avec succès", + "@messageHaveBeenDeletedSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "error": "Erreur", + "@error": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "connectedToTheInternet": "Connecté à Internet", + "@connectedToTheInternet": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyTrash": "Vider la corbeille", + "@emptyTrash": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "cannotSelectThisImage": "Impossible de sélectionner cette image.", + "@cannotSelectThisImage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHasBeenSavedToTheSendingQueue": "Le message a été enregistré dans la file d'attente.", + "@messageHasBeenSavedToTheSendingQueue": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bannerMessageSendingQueueView": "Les messages de la file d'attente seront envoyés ou planifiés lorsqu'ils seront en ligne.", + "@bannerMessageSendingQueueView": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "proceed": "Procéder", + "@proceed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youAreInOfflineMode": "Vous êtes en mode hors ligne", + "@youAreInOfflineMode": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailSecond": " envoyer, répondre ou transférer ", + "@messageDialogWhenStoreSendingEmailSecond": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailFirst": "Heureusement, vous pouvez toujours", + "@messageDialogWhenStoreSendingEmailFirst": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailThird": "des messages. Ils seront délivrés lorsque vous vous connecterez à Internet. Pour modifier ces messages avant de les envoyer, rendez-vous sur ", + "@messageDialogWhenStoreSendingEmailThird": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleRecipientSendingEmail": "À : {recipients}", + "@titleRecipientSendingEmail": { + "type": "text", + "placeholders_order": [ + "recipients" + ], + "placeholders": { + "recipients": {} + } + }, + "openMailboxMenu": "Ouvrir le menu de la boîte aux lettres", + "@openMailboxMenu": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHasBeenSentSuccessfully": "Le message a été envoyé avec succès.", + "@messageHasBeenSentSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteOfflineEmail": "Supprimer les messages hors-ligne", + "@deleteOfflineEmail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendingQueue": "File d'attente", + "@sendingQueue": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailTail": " boîte aux lettres.", + "@messageDialogWhenStoreSendingEmailTail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delivering": "Livraison en cours", + "@delivering": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "resend": "Renvoyer", + "@resend": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messagesHaveBeenResent": "Les messages ont été renvoyés", + "@messagesHaveBeenResent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "connectionError": "Erreur de connexion", + "@connectionError": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "spamMailboxDisplayName": "Spam", + "@spamMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "draftsMailboxDisplayName": "Brouillons", + "@draftsMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "trashMailboxDisplayName": "Corbeille", + "@trashMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "templatesMailboxDisplayName": "Modèles", + "@templatesMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "archiveMailboxDisplayName": "Archives", + "@archiveMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "outboxMailboxDisplayName": "Boîte d'envoi", + "@outboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "forwardedMessage": "Message transféré", + "@forwardedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "repliedMessage": "Message répondu", + "@repliedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "repliedAndForwardedMessage": "Message répondu et transféré", + "@repliedAndForwardedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyTrashMessageDialog": "Vous êtes sur le point de supprimer définitivement tous les éléments de la corbeille . Voulez-vous continuer ?", + "@emptyTrashMessageDialog": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bannerDeleteAllSpamEmailsMessage": "Tous les messages dans le dossier Spam seront supprimés si vous atteignez votre limite de stockage.", + "@bannerDeleteAllSpamEmailsMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeDeclined": " a décliné cette invitation", + "@messageEventActionBannerAttendeeDeclined": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "link": "Lien", + "@link": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "seeAllAttendees": "Voir tous les participants", + "@seeAllAttendees": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeCounterDeclined": "Votre contre-proposition a été refusée", + "@messageEventActionBannerAttendeeCounterDeclined": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerOrganizerCanceled": " a annulé une réunion", + "@messageEventActionBannerOrganizerCanceled": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "organizer": "Organisateur", + "@organizer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "where": "Où", + "@where": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quotaWarningBannerMessage": "Bientôt, vous ne pourrez plus envoyer d'e-mails dans Tmail. Veuillez nettoyer votre stockage ou mettre à niveau votre stockage pour bénéficier de toutes les fonctionnalités de Tmail.", + "@quotaWarningBannerMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptySpamFolder": "Vider le dossier Spam", + "@emptySpamFolder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subMessageEventActionBannerCanceled": "\"Nous annulons l'événement en raison du mauvais temps.\"", + "@subMessageEventActionBannerCanceled": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteAllSpamEmails": "Supprimer tous les spams", + "@deleteAllSpamEmails": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "anAttendee": "Un participant", + "@anAttendee": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "createFilters": "Créer des filtres", + "@createFilters": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptySpamMessageDialog": "Vous êtes sur le point de supprimer définitivement tous les emails du dossier spam. Voulez-vous continuer ?", + "@emptySpamMessageDialog": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "location": "Lieu", + "@location": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeTentative": " a répondu \"Peut-être\" à cette invitation", + "@messageEventActionBannerAttendeeTentative": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attendees": "Participants", + "@attendees": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "invitationMessageCalendarInformation": " vous a invité à une réunion :", + "@invitationMessageCalendarInformation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "who": "Qui", + "@who": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "maybe": "Peut être", + "@maybe": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteAllSpamEmailsNow": "Supprimez tous les spams maintenant", + "@deleteAllSpamEmailsNow": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pleaseChooseAnImageSizeCorrectly": "Merci de choisir une taille d'image <= {maxSize}KB", + "@pleaseChooseAnImageSizeCorrectly": { + "type": "text", + "placeholders_order": [ + "maxSize" + ], + "placeholders": { + "maxSize": {} + } + }, + "quotaErrorBannerTitle": "Vous n'avez plus d'espace de stockage libre", + "@quotaErrorBannerTitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quotaErrorBannerMessage": "Bientôt, vous ne pourrez plus envoyer d'e-mails dans Tmail. Veuillez nettoyer votre stockage ou mettre à niveau votre stockage pour bénéficier de toutes les fonctionnalités de Tmail.", + "@quotaErrorBannerMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeAccepted": " a accepté cette invitation", + "@messageEventActionBannerAttendeeAccepted": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "you": "Vous", + "@you": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerOrganizerInvited": " vous a invité à une réunion", + "@messageEventActionBannerOrganizerInvited": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quotaWarningBannerTitle": "Vous manquez d'espace de stockage (90%).", + "@quotaWarningBannerTitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subMessageEventActionBannerUpdated": "\"L'heure a été mise à jour pour mieux convenir à chacun d'entre vous\"", + "@subMessageEventActionBannerUpdated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "folderCreatedTitle": "Votre dossier vient d'être créé", + "@folderCreatedTitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerOrganizerUpdated": " a mis à jour une réunion", + "@messageEventActionBannerOrganizerUpdated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quotaStateLabel": "{used} sur {limit} utilisé", + "@quotaStateLabel": { + "type": "text", + "placeholders_order": [ + "used", + "limit" + ], + "placeholders": { + "used": {}, + "limit": {} + } + }, + "createFolderSuccessfullyMessage": "Vous avez créé avec succès le dossier {folderName}", + "@createFolderSuccessfullyMessage": { + "type": "text", + "placeholders_order": [ + "folderName" + ], + "placeholders": { + "folderName": {} + } + }, + "time": "Temps", + "@time": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "folderCreatedMessage": "Pour commencer à utiliser cette boîte aux lettres, vous pouvez ajouter quelques règles pour organiser tout votre courrier à votre manière.", + "@folderCreatedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "when": "Quand", + "@when": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeCounter": " a proposé des modifications à l'événement", + "@messageEventActionBannerAttendeeCounter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/l10n/intl_ga.arb b/lib/l10n/intl_ga.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_ga.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_gl.arb b/lib/l10n/intl_gl.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_gl.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_he.arb b/lib/l10n/intl_he.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_he.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_hi.arb b/lib/l10n/intl_hi.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_hi.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_hu.arb b/lib/l10n/intl_hu.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_hu.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_id.arb b/lib/l10n/intl_id.arb new file mode 100644 index 0000000000..b60b255b99 --- /dev/null +++ b/lib/l10n/intl_id.arb @@ -0,0 +1,20 @@ +{ + "initializing_data": "Memuat data...", + "@initializing_data": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "login_text_slogan": "Pesan Tim", + "@login_text_slogan": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyListEmailForward": "Silakan masukkan penerima pesan", + "@emptyListEmailForward": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + } +} diff --git a/lib/l10n/intl_ie.arb b/lib/l10n/intl_ie.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_ie.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 9e7902adb1..561758c140 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -2654,5 +2654,253 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "reply_to": "Rispondi a", + "@reply_to": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bcc_to": "Bcc a", + "@bcc_to": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "html": "Html", + "@html": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "regards": "Saluti", + "@regards": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youHaveNewMessages": "Hai nuovi messaggi", + "@youHaveNewMessages": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "appTitlePushNotification": "Team Mail", + "@appTitlePushNotification": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "totalNewMessagePushNotification": "{count} nuove email", + "@totalNewMessagePushNotification": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "countNewSpamEmails": "Hai {count} nuove email di spam!", + "@countNewSpamEmails": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "showDetails": "Mostra dettagli", + "@showDetails": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dismiss": "Cancellare", + "@dismiss": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "privacyPolicy": "Politica sulla riservatezza", + "@privacyPolicy": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "disableSpamReport": "Disattivare rapporto di spam", + "@disableSpamReport": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "enableSpamReport": "Attivare rapporto di spam", + "@enableSpamReport": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "required": "necessario", + "@required": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEmailInYourCurrentMailbox": "Siamo spiacenti, non ci sono email nella tua casella di posta attuale", + "@noEmailInYourCurrentMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEmailMatchYourCurrentFilter": "Siamo spiacenti, non ci sono email che corrispondono al tuo filtro attuale.", + "@noEmailMatchYourCurrentFilter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "reduceSomeFiltersAndTryAgain": "Riduciamo alcuni filtri e riproviamo", + "@reduceSomeFiltersAndTryAgain": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailure": "Impossibile inviare il messaggio.", + "@sendMessageFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailureWithSetErrorTypeTooLarge": "Impossibile inviare il messaggio perché è troppo grande.", + "@sendMessageFailureWithSetErrorTypeTooLarge": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailBoxes": "Caselle di posta", + "@mailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailure": "Impossibile salvare il messaggio come bozza.", + "@saveEmailAsDraftFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailureWithSetErrorTypeTooLarge": "Impossibile salvare il messaggio come bozza perché è troppo grande.", + "@saveEmailAsDraftFailureWithSetErrorTypeTooLarge": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailureWithSetErrorTypeOverQuota": "Impossibile salvare il messaggio come bozza perché ha superato la quota.", + "@saveEmailAsDraftFailureWithSetErrorTypeOverQuota": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "thisImageCannotBeAdded": "Questa immagine non può essere aggiunta.", + "@thisImageCannotBeAdded": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMsgHideMailboxSuccess": "Questa casella di posta è stata nascosta dalla tua casella di posta principale", + "@toastMsgHideMailboxSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "searchForMailboxes": "Cerca le caselle di posta", + "@searchForMailboxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "showMailbox": "Mostra caselle di posta", + "@showMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailboxVisibility": "Visibilità della casella di posta", + "@mailboxVisibility": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailboxVisibilitySubtitle": "Mostra/nascondi le tue caselle di posta, comprese quelle personali e del team.", + "@mailboxVisibilitySubtitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "personalFolders": "Cartelle personali", + "@personalFolders": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageShowMailboxSuccess": "Questa casella di posta è già visualizzata nella tua casella di posta principale", + "@toastMessageShowMailboxSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Bad credentials": "Credenziali sbagliate", + "@Bad credentials": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "profilesSettingExplanation": "Tue informazioni e opzioni per gestirle.", + "@profilesSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "setDefaultIdentity": "Imposta come identità predefinita", + "@setDefaultIdentity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "identitiesSettingExplanation": "Seleziona l'identità o l'indirizzo e-mail che desideri utilizzare per inviare e-mail", + "@identitiesSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "createNewIdentity": "Crea una nuova identità", + "@createNewIdentity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "you_have_created_a_new_default_identity": "Hai creato una nuova identità predefinita", + "@you_have_created_a_new_default_identity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailureWithSetErrorTypeOverQuota": "Impossibile inviare il messaggio perché ha superato la quota.", + "@sendMessageFailureWithSetErrorTypeOverQuota": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "teamMailBoxes": "Caselle di posta del team", + "@teamMailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hideMailBoxes": "Nascondere caselle di posta", + "@hideMailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/l10n/intl_ko.arb b/lib/l10n/intl_ko.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_ko.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_lt.arb b/lib/l10n/intl_lt.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_lt.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_messages.arb b/lib/l10n/intl_messages.arb index 572dd097d2..436c876663 100644 --- a/lib/l10n/intl_messages.arb +++ b/lib/l10n/intl_messages.arb @@ -1,5 +1,5 @@ { - "@@last_modified": "2023-02-10T03:40:25.843946", + "@@last_modified": "2023-10-05T15:38:07.324902", "initializing_data": "Initializing data...", "@initializing_data": { "type": "text", @@ -24,6 +24,12 @@ "placeholders_order": [], "placeholders": {} }, + "Bad credentials": "Bad credentials", + "@Bad credentials": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "next": "Next", "@next": { "type": "text", @@ -120,8 +126,8 @@ "placeholders_order": [], "placeholders": {} }, - "personalMailboxes": "Personal mailboxes", - "@personalMailboxes": { + "personalFolders": "Personal folders", + "@personalFolders": { "type": "text", "placeholders_order": [], "placeholders": {} @@ -1154,7 +1160,7 @@ "placeholders_order": [], "placeholders": {} }, - "profilesSettingExplanation": "Info about you, and options to manage it", + "profilesSettingExplanation": "Info about you, and options to manage it.", "@profilesSettingExplanation": { "type": "text", "placeholders_order": [], @@ -1214,12 +1220,6 @@ "placeholders_order": [], "placeholders": {} }, - "plain_text": "Plain text", - "@plain_text": { - "type": "text", - "placeholders_order": [], - "placeholders": {} - }, "html_template": "Html template", "@html_template": { "type": "text", @@ -1488,12 +1488,6 @@ "placeholders_order": [], "placeholders": {} }, - "allMails": "All mails", - "@allMails": { - "type": "text", - "placeholders_order": [], - "placeholders": {} - }, "allTime": "All time", "@allTime": { "type": "text", @@ -1572,6 +1566,18 @@ "placeholders_order": [], "placeholders": {} }, + "languageArabic": "Arabic", + "@languageArabic": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageItalian": "Italian", + "@languageItalian": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, "messageDialogSendEmailUploadingAttachment": "Your message could not be sent because it uploading attachment", "@messageDialogSendEmailUploadingAttachment": { "type": "text", @@ -1680,12 +1686,6 @@ "placeholders_order": [], "placeholders": {} }, - "codeView": "Code view", - "@codeView": { - "type": "text", - "placeholders_order": [], - "placeholders": {} - }, "headerStyle": "Style", "@headerStyle": { "type": "text", @@ -2476,52 +2476,12 @@ "placeholders_order": [], "placeholders": {} }, - "textQuotasUsed": "{used} GB of {softLimit} GB Used", - "@textQuotasUsed": { - "type": "text", - "placeholders_order": [ - "used", - "softLimit" - ], - "placeholders": { - "used": {}, - "softLimit": {} - } - }, - "textQuotasRunningOutOfStorageTitle": "You are running out of storage ({progress}%).", - "@textQuotasRunningOutOfStorageTitle": { - "type": "text", - "placeholders_order": [ - "progress" - ], - "placeholders": { - "progress": {} - } - }, - "textQuotasRunningOutOfStorageContent": "Soon you won't be able to email in Team Mail. Please clean your storage or upgrade your storage to get full features in Team Mail.", - "@textQuotasRunningOutOfStorageContent": { - "type": "text", - "placeholders_order": [], - "placeholders": {} - }, "textQuotasOutOfStorage": "Out of storage", "@textQuotasOutOfStorage": { "type": "text", "placeholders_order": [], "placeholders": {} }, - "textQuotasRunOutOfStorageTitle": "You have run out of storage space", - "@textQuotasRunOutOfStorageTitle": { - "type": "text", - "placeholders_order": [], - "placeholders": {} - }, - "textQuotasRunOutOfStorageContent": "Now you temporarily can't send or get an email. Please free up or upgrade your storage to get the full features of Team Mail.", - "@textQuotasRunOutOfStorageContent": { - "type": "text", - "placeholders_order": [], - "placeholders": {} - }, "quickCreatingRule": "Create a rule with this email", "@quickCreatingRule": { "type": "text", @@ -2727,5 +2687,575 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "mailboxVisibility": "Mailbox visibility", + "@mailboxVisibility": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailboxVisibilitySubtitle": "Show/ hide your mailboxes, including your personal and team mailboxes.", + "@mailboxVisibilitySubtitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyListEmailForward": "Please input at least one recipient", + "@emptyListEmailForward": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "forwardedMessage": "Forwarded message", + "@forwardedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "repliedMessage": "Replied message", + "@repliedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "repliedAndForwardedMessage": "Replied and Forwarded message", + "@repliedAndForwardedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyTrash": "Empty Trash", + "@emptyTrash": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyTrashMessageDialog": "You are about to permanently delete all items in Trash . Do you want to continue?", + "@emptyTrashMessageDialog": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "cannotSelectThisImage": "Cannot select this image.", + "@cannotSelectThisImage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHasBeenSavedToTheSendingQueue": "Message has been saved to the sending queue.", + "@messageHasBeenSavedToTheSendingQueue": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendingQueue": "Sending queue", + "@sendingQueue": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bannerMessageSendingQueueView": "Messages in Sending queue mailbox will be sent or scheduled when online.", + "@bannerMessageSendingQueueView": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "proceed": "Proceed", + "@proceed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youAreInOfflineMode": "You're in offline mode", + "@youAreInOfflineMode": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailFirst": "Fortunately, you can still", + "@messageDialogWhenStoreSendingEmailFirst": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailSecond": " send, reply, or forward ", + "@messageDialogWhenStoreSendingEmailSecond": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailThird": "emails. They will be delivered when you connect to the internet. To edit these emails before sending, go to the ", + "@messageDialogWhenStoreSendingEmailThird": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailTail": " mailbox.", + "@messageDialogWhenStoreSendingEmailTail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleRecipientSendingEmail": "To: {recipients}", + "@titleRecipientSendingEmail": { + "type": "text", + "placeholders_order": [ + "recipients" + ], + "placeholders": { + "recipients": {} + } + }, + "openMailboxMenu": "Open mailbox menu", + "@openMailboxMenu": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHasBeenSentSuccessfully": "Message has been sent successfully.", + "@messageHasBeenSentSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteOfflineEmail": "Delete offline email", + "@deleteOfflineEmail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogDeleteSendingEmail": "Deleting an offline email will erase its content permanently. You won't be able to undo this action or recover the email from the Trash mailbox.", + "@messageDialogDeleteSendingEmail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHaveBeenDeletedSuccessfully": "Messages have been deleted successfully", + "@messageHaveBeenDeletedSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delivering": "Delivering", + "@delivering": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "error": "Error", + "@error": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "connectedToTheInternet": "Connected to the internet", + "@connectedToTheInternet": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "resend": "Resend", + "@resend": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messagesHaveBeenResent": "Messages have been resent", + "@messagesHaveBeenResent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "connectionError": "Connection error", + "@connectionError": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "inboxMailboxDisplayName": "Inbox", + "@inboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sentMailboxDisplayName": "Sent", + "@sentMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "outboxMailboxDisplayName": "Outbox", + "@outboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "spamMailboxDisplayName": "Spam", + "@spamMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "draftsMailboxDisplayName": "Drafts", + "@draftsMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "trashMailboxDisplayName": "Trash", + "@trashMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "templatesMailboxDisplayName": "Templates", + "@templatesMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "archiveMailboxDisplayName": "Archive", + "@archiveMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pleaseChooseAnImageSizeCorrectly": "Please choose an image size <= {maxSize}KB", + "@pleaseChooseAnImageSizeCorrectly": { + "type": "text", + "placeholders_order": [ + "maxSize" + ], + "placeholders": { + "maxSize": {} + } + }, + "messageEventActionBannerOrganizerInvited": " has invited you in to a meeting", + "@messageEventActionBannerOrganizerInvited": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerOrganizerUpdated": " has updated a meeting", + "@messageEventActionBannerOrganizerUpdated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerOrganizerCanceled": " has canceled a meeting", + "@messageEventActionBannerOrganizerCanceled": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subMessageEventActionBannerUpdated": "\"The time has been updated to better suit all of you\"", + "@subMessageEventActionBannerUpdated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subMessageEventActionBannerCanceled": "\"We are canceling the event due to bad weather.\"", + "@subMessageEventActionBannerCanceled": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "anAttendee": "An attendee", + "@anAttendee": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "you": "You", + "@you": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeAccepted": " has accepted this invitation", + "@messageEventActionBannerAttendeeAccepted": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeTentative": " has replied \"Maybe\" to this invitation", + "@messageEventActionBannerAttendeeTentative": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeDeclined": " has declined this invitation", + "@messageEventActionBannerAttendeeDeclined": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeCounter": " has proposed changes to the event", + "@messageEventActionBannerAttendeeCounter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeCounterDeclined": "Your counter proposal was declined", + "@messageEventActionBannerAttendeeCounterDeclined": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "invitationMessageCalendarInformation": " has invited you in to a meeting:", + "@invitationMessageCalendarInformation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "when": "When", + "@when": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "where": "Where", + "@where": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "who": "Who", + "@who": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "organizer": "Organizer", + "@organizer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "time": "Time", + "@time": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "location": "Location", + "@location": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attendees": "Attendees", + "@attendees": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "seeAllAttendees": "See all attendees", + "@seeAllAttendees": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "link": "Link", + "@link": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteAllSpamEmails": "Delete all spam emails", + "@deleteAllSpamEmails": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptySpamFolder": "Empty Spam folder", + "@emptySpamFolder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptySpamMessageDialog": "You are about to permanently delete all items in Spam . Do you want to continue?", + "@emptySpamMessageDialog": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bannerDeleteAllSpamEmailsMessage": "All messages in Spam will be deleted if you reach limited storage.", + "@bannerDeleteAllSpamEmailsMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteAllSpamEmailsNow": "Delete all spam emails now", + "@deleteAllSpamEmailsNow": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quotaStateLabel": "{used} of {limit} Used", + "@quotaStateLabel": { + "type": "text", + "placeholders_order": [ + "used", + "limit" + ], + "placeholders": { + "used": {}, + "limit": {} + } + }, + "quotaErrorBannerTitle": "You have run out of storage space", + "@quotaErrorBannerTitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quotaWarningBannerTitle": "You are running out of storage (90%).", + "@quotaWarningBannerTitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quotaWarningBannerMessage": "Soon you won't be able to email in Tmail. Please clean your storage or upgrade your storage to get full features in Tmail.", + "@quotaWarningBannerMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quotaErrorBannerMessage": "Soon you won't be able to email in Tmail. Please clean your storage or upgrade your storage to get full features in Tmail.", + "@quotaErrorBannerMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "createFolderSuccessfullyMessage": "You successfully created {folderName} folder", + "@createFolderSuccessfullyMessage": { + "type": "text", + "placeholders_order": [ + "folderName" + ], + "placeholders": { + "folderName": {} + } + }, + "folderCreatedTitle": "Your folder is just created", + "@folderCreatedTitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "folderCreatedMessage": "To begin using this mailbox, you should add some rules to organize all of your mail in your own way.", + "@folderCreatedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "createFilters": "Create filters", + "@createFilters": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "maybe": "Maybe", + "@maybe": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "enterASubject": "Enter a subject", + "@enterASubject": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "enterSomeSuggestions": "Enter some suggestions", + "@enterSomeSuggestions": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "markedSingleMessageToast": "Message has been marked as {action}", + "@markedSingleMessageToast": { + "type": "text", + "placeholders_order": [ + "action" + ], + "placeholders": { + "action": {} + } + }, + "empty": "Empty", + "@empty": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "clearFolder": "Clear folder", + "@clearFolder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEmptyFolderDialog": "The messages in {folder} folder will be permanently deleted and you will not be able to restore them", + "@messageEmptyFolderDialog": { + "type": "text", + "placeholders_order": [ + "folder" + ], + "placeholders": { + "folder": {} + } + }, + "addCondition": "Add condition", + "@addCondition": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "formattingOptions": "Formatting options", + "@formattingOptions": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "embedCode": "Embed code", + "@embedCode": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "showMoreAttachment": "Show more (+{count})", + "@showMoreAttachment": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "saveAsDraft": "Save as draft", + "@saveAsDraft": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dropFileHereToAttachThem": "Drop file here to attach them", + "@dropFileHereToAttachThem": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "canceled": "Canceled", + "@canceled": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "newSubfolder": "New subfolder", + "@newSubfolder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } \ No newline at end of file diff --git a/lib/l10n/intl_pt_PT.arb b/lib/l10n/intl_pt_PT.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_pt_PT.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_ro.arb b/lib/l10n/intl_ro.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_ro.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 32e1c355a1..71899f27f7 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -2655,5 +2655,727 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "personalFolders": "Личные папки", + "@personalFolders": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "profilesSettingExplanation": "Информация профиля и управление.", + "@profilesSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "identitiesSettingExplanation": "Выберите идентификатор или адрес электронной почты, который вы хотите использовать для отправки писем", + "@identitiesSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "regards": "С уважением", + "@regards": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youHaveNewMessages": "У вас есть новые сообщения", + "@youHaveNewMessages": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "appTitlePushNotification": "Team Mail", + "@appTitlePushNotification": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "reduceSomeFiltersAndTryAgain": "Снимите часть фильтров и попробуйте еще раз", + "@reduceSomeFiltersAndTryAgain": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailure": "Не удалось отправить сообщение.", + "@sendMessageFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailureWithSetErrorTypeTooLarge": "Не удалось отправить сообщение, так как превышена его максимальная длина.", + "@sendMessageFailureWithSetErrorTypeTooLarge": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailure": "Не удалось сохранить сообщение в качестве черновика.", + "@saveEmailAsDraftFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailBoxes": "Почтовые ящики", + "@mailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "teamMailBoxes": "Общие почтовые ящики", + "@teamMailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hideMailBoxes": "Скрыть почтовый ящик", + "@hideMailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "thisImageCannotBeAdded": "Невозможно добавить это изображение.", + "@thisImageCannotBeAdded": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMsgHideMailboxSuccess": "Почтовый ящик скрыт из основного почтового ящика", + "@toastMsgHideMailboxSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "searchForMailboxes": "Поиск почтовых ящиков", + "@searchForMailboxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "showMailbox": "Показать почтовый ящик", + "@showMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailboxVisibilitySubtitle": "Показать/скрыть почтовые ящики, включая личные и общие почтовые ящики.", + "@mailboxVisibilitySubtitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "setDefaultIdentity": "Сделать идентификатором по умолчанию", + "@setDefaultIdentity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "totalNewMessagePushNotification": "{count} новых письма(-ем)", + "@totalNewMessagePushNotification": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "privacyPolicy": "Политика конфиденциальности", + "@privacyPolicy": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailureWithSetErrorTypeOverQuota": "Не удалось сохранить сообщение как черновик, так как оно превысило квоту.", + "@saveEmailAsDraftFailureWithSetErrorTypeOverQuota": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailureWithSetErrorTypeTooLarge": "Не удалось сохранить сообщение как черновик, так как превышена его максимальная длина.", + "@saveEmailAsDraftFailureWithSetErrorTypeTooLarge": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageShowMailboxSuccess": "Этот почтовый ящик уже отображается в основном почтовом ящике", + "@toastMessageShowMailboxSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Bad credentials": "Неверные данные", + "@Bad credentials": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "createNewIdentity": "Создать новый идентификатор", + "@createNewIdentity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "reply_to": "Ответить", + "@reply_to": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bcc_to": "Bcc", + "@bcc_to": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "html": "Html", + "@html": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "you_have_created_a_new_default_identity": "Вы создали новый идентификатор по умолчанию", + "@you_have_created_a_new_default_identity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "countNewSpamEmails": "У вас {count} новых письма(-ем) со спамом!", + "@countNewSpamEmails": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "showDetails": "Подробнее", + "@showDetails": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dismiss": "Отменить", + "@dismiss": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "disableSpamReport": "Отключить отчеты о спаме", + "@disableSpamReport": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "enableSpamReport": "Включить отчеты о спаме", + "@enableSpamReport": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "required": "требуется", + "@required": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEmailInYourCurrentMailbox": "К сожалению, в текущем почтовом ящике нет писем", + "@noEmailInYourCurrentMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEmailMatchYourCurrentFilter": "К сожалению, мы не нашли писем, соответствующих текущему фильтру.", + "@noEmailMatchYourCurrentFilter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailureWithSetErrorTypeOverQuota": "Не удалось отправить сообщение, так как оно превышает квоту.", + "@sendMessageFailureWithSetErrorTypeOverQuota": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailboxVisibility": "Отображение почтового ящика", + "@mailboxVisibility": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageArabic": "Арабский", + "@languageArabic": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "forwardedMessage": "Пересланное сообщение", + "@forwardedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyTrashMessageDialog": "Вы собираетесь безвозвратно удалить все элементы из корзины. Продолжить?", + "@emptyTrashMessageDialog": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "repliedMessage": "Сообщение с ответом", + "@repliedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "repliedAndForwardedMessage": "Сообщение с ответом и пересланное сообщение", + "@repliedAndForwardedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyTrash": "Очистить Корзину", + "@emptyTrash": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHasBeenSavedToTheSendingQueue": "Сообщение сохранено в очереди отправки.", + "@messageHasBeenSavedToTheSendingQueue": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bannerMessageSendingQueueView": "Сообщения в очереди будут отправлены, когда вы вернетесь онлайн.", + "@bannerMessageSendingQueueView": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "proceed": "Продолжить", + "@proceed": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youAreInOfflineMode": "Вы в оффлайн-режиме", + "@youAreInOfflineMode": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailSecond": " отправлять, отвечать или пересылать ", + "@messageDialogWhenStoreSendingEmailSecond": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailTail": " почтовый ящик.", + "@messageDialogWhenStoreSendingEmailTail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "openMailboxMenu": "Открыть меню почтового ящика", + "@openMailboxMenu": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteOfflineEmail": "Удалить оффлайн-письмо", + "@deleteOfflineEmail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "error": "Ошибка", + "@error": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messagesHaveBeenResent": "Сообщения отправлены еще раз", + "@messagesHaveBeenResent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "cannotSelectThisImage": "Невозможно выбрать это изображение.", + "@cannotSelectThisImage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendingQueue": "Очередь отправки", + "@sendingQueue": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailFirst": "К счастью, вы все еще можете", + "@messageDialogWhenStoreSendingEmailFirst": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogWhenStoreSendingEmailThird": "письма. Они будут доставлены при подключении к Интернету. Чтобы отредактировать эти электронные письма перед отправкой, перейдите в ", + "@messageDialogWhenStoreSendingEmailThird": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleRecipientSendingEmail": "Кому: {recipients}", + "@titleRecipientSendingEmail": { + "type": "text", + "placeholders_order": [ + "recipients" + ], + "placeholders": { + "recipients": {} + } + }, + "messageHasBeenSentSuccessfully": "Сообщение успешно отправлено.", + "@messageHasBeenSentSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageDialogDeleteSendingEmail": "Удаление оффлайн-письма приведет к безвозвратному удалению его содержимого. Вы не сможете отменить это действие или восстановить письмо из «Корзины».", + "@messageDialogDeleteSendingEmail": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "resend": "Отправить еще раз", + "@resend": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "trashMailboxDisplayName": "Корзина", + "@trashMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "templatesMailboxDisplayName": "Шаблоны", + "@templatesMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageHaveBeenDeletedSuccessfully": "Сообщения успешно удалены", + "@messageHaveBeenDeletedSuccessfully": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "delivering": "Доставляется", + "@delivering": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "connectedToTheInternet": "Подключен к Интернету", + "@connectedToTheInternet": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "inboxMailboxDisplayName": "Входящие", + "@inboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "connectionError": "Ошибка подключения", + "@connectionError": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sentMailboxDisplayName": "Отправленные", + "@sentMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "archiveMailboxDisplayName": "Архив", + "@archiveMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageItalian": "Итальянский", + "@languageItalian": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyListEmailForward": "Укажите хотя бы одного получателя", + "@emptyListEmailForward": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "outboxMailboxDisplayName": "Исходящие", + "@outboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "spamMailboxDisplayName": "Спам", + "@spamMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "draftsMailboxDisplayName": "Черновики", + "@draftsMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bannerDeleteAllSpamEmailsMessage": "Все сообщения в папке \"Спам\" будут удалены, если вы достигните лимита хранилища.", + "@bannerDeleteAllSpamEmailsMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeDeclined": " отклонил(а) это приглашение", + "@messageEventActionBannerAttendeeDeclined": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "link": "Ссылка", + "@link": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "seeAllAttendees": "Посмотреть всех участников", + "@seeAllAttendees": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeCounterDeclined": "Ваше предложение отклонено", + "@messageEventActionBannerAttendeeCounterDeclined": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerOrganizerCanceled": " отменил(а) встречу", + "@messageEventActionBannerOrganizerCanceled": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "organizer": "Организатор", + "@organizer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "where": "Где", + "@where": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quotaWarningBannerMessage": "Скоро вы не сможете писать письма в Tmail. Пожалуйста, очистите хранилище или обновите план, чтобы получить все возможности Tmail.", + "@quotaWarningBannerMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptySpamFolder": "Очистить папку \"Спам\"", + "@emptySpamFolder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subMessageEventActionBannerCanceled": "«Мероприятие отменено из-за плохой погоды.»", + "@subMessageEventActionBannerCanceled": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteAllSpamEmails": "Удалить все письма из спама", + "@deleteAllSpamEmails": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "anAttendee": "Участник", + "@anAttendee": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "createFilters": "Создать фильтры", + "@createFilters": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptySpamMessageDialog": "Вы собираетесь безвозвратно удалить все элементы из папки \"Спам\". Продолжить?", + "@emptySpamMessageDialog": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "location": "Место встречи", + "@location": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeTentative": " ответил(а) \"Возможно\" на это приглашение", + "@messageEventActionBannerAttendeeTentative": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "attendees": "Участники", + "@attendees": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "invitationMessageCalendarInformation": " пригласил(а) вас на встречу:", + "@invitationMessageCalendarInformation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "who": "Кто", + "@who": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "maybe": "Возможно", + "@maybe": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "deleteAllSpamEmailsNow": "Удалить все спам-письма", + "@deleteAllSpamEmailsNow": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "pleaseChooseAnImageSizeCorrectly": "Пожалуйста, выберите размер изображения <= {maxSize} КБ", + "@pleaseChooseAnImageSizeCorrectly": { + "type": "text", + "placeholders_order": [ + "maxSize" + ], + "placeholders": { + "maxSize": {} + } + }, + "quotaErrorBannerTitle": "Вы достигли лимита хранилища", + "@quotaErrorBannerTitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quotaErrorBannerMessage": "Скоро вы не сможете писать письма в Tmail. Пожалуйста, очистите хранилище или обновите план, чтобы получить все возможности Tmail.", + "@quotaErrorBannerMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeAccepted": " принял(а) это приглашение", + "@messageEventActionBannerAttendeeAccepted": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "you": "Вы", + "@you": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerOrganizerInvited": " пригласил(а) вас на встречу", + "@messageEventActionBannerOrganizerInvited": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quotaWarningBannerTitle": "У вас заканчивается место (90%).", + "@quotaWarningBannerTitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subMessageEventActionBannerUpdated": "«Время изменено, теперь оно лучше подходит всем»", + "@subMessageEventActionBannerUpdated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "folderCreatedTitle": "Ваша папка создана", + "@folderCreatedTitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerOrganizerUpdated": " обновил(а) встречу", + "@messageEventActionBannerOrganizerUpdated": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quotaStateLabel": "Использовано {used} из {limit}", + "@quotaStateLabel": { + "type": "text", + "placeholders_order": [ + "used", + "limit" + ], + "placeholders": { + "used": {}, + "limit": {} + } + }, + "createFolderSuccessfullyMessage": "Вы успешно создали папку {folderName}", + "@createFolderSuccessfullyMessage": { + "type": "text", + "placeholders_order": [ + "folderName" + ], + "placeholders": { + "folderName": {} + } + }, + "time": "Время", + "@time": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "folderCreatedMessage": "Чтобы начать использовать этот почтовый ящик, добавьте несколько правил для персонализации.", + "@folderCreatedMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "when": "Когда", + "@when": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "messageEventActionBannerAttendeeCounter": " предложил(а) изменения к этому событию", + "@messageEventActionBannerAttendeeCounter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/l10n/intl_sk.arb b/lib/l10n/intl_sk.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_sk.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_ta.arb b/lib/l10n/intl_ta.arb new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/lib/l10n/intl_ta.arb @@ -0,0 +1 @@ +{} diff --git a/lib/l10n/intl_vi.arb b/lib/l10n/intl_vi.arb index ab26b5d8f9..87f7d1f679 100644 --- a/lib/l10n/intl_vi.arb +++ b/lib/l10n/intl_vi.arb @@ -1,6 +1,6 @@ { "@@last_modified": "2021-10-13T22:37:55.795262", - "initializing_data": "Khởi tạo …", + "initializing_data": "Đang khởi tạo dữ liệu...", "@initializing_data": { "type": "text", "placeholders_order": [], @@ -150,7 +150,7 @@ "placeholders_order": [], "placeholders": {} }, - "header_email_quoted": "On {sentDate}, from {emailAddress}", + "header_email_quoted": "Vào ngày {sentDate}, từ {emailAddress}", "@header_email_quoted": { "type": "text", "placeholders_order": [ @@ -168,7 +168,7 @@ "placeholders_order": [], "placeholders": {} }, - "prefix_forward_email": "Fwd:", + "prefix_forward_email": "Chuyển tiếp:", "@prefix_forward_email": { "type": "text", "placeholders_order": [], @@ -254,7 +254,7 @@ "count": {} } }, - "marked_multiple_item_as_unread": "Marked {count} item as unread", + "marked_multiple_item_as_unread": "Đánh dấu {count} email chưa đọc", "@marked_multiple_item_as_unread": { "type": "text", "placeholders_order": [ @@ -264,13 +264,13 @@ "count": {} } }, - "an_error_occurred": "Error! An error occurred. Please try again later.", + "an_error_occurred": "Lỗi! Đã xảy ra lỗi. Vui lòng thử lại sau.", "@an_error_occurred": { "type": "text", "placeholders_order": [], "placeholders": {} }, - "attachment_download_failed": "Attachment download failed", + "attachment_download_failed": "Tải xuống tệp đính kèm không thành công", "@attachment_download_failed": { "type": "text", "placeholders_order": [], @@ -298,13 +298,13 @@ "placeholders_order": [], "placeholders": {} }, - "user_cancel_download_file": "User cancel download file", + "user_cancel_download_file": "Người dùng hủy tải xuống", "@user_cancel_download_file": { "type": "text", "placeholders_order": [], "placeholders": {} }, - "you_need_to_grant_files_permission_to_download_attachments": "You need to grant files permission to download attachments", + "you_need_to_grant_files_permission_to_download_attachments": "Bạn cần được cấp quyền tải xuống tệp đính kèm", "@you_need_to_grant_files_permission_to_download_attachments": { "type": "text", "placeholders_order": [], @@ -320,25 +320,25 @@ "count": {} } }, - "attach_file_prepare_text": "Preparing to attach file...", + "attach_file_prepare_text": "Đang chuẩn bị đính kèm tệp...", "@attach_file_prepare_text": { "type": "text", "placeholders_order": [], "placeholders": {} }, - "can_not_upload_this_file_as_attachments": "Can not upload this file as attachments", + "can_not_upload_this_file_as_attachments": "Không thể tải tệp này lên dưới dạng tệp đính kèm", "@can_not_upload_this_file_as_attachments": { "type": "text", "placeholders_order": [], "placeholders": {} }, - "attachments_uploaded_successfully": "Attachments uploaded successfully", + "attachments_uploaded_successfully": "Tệp đính kèm đã được tải lên thành công", "@attachments_uploaded_successfully": { "type": "text", "placeholders_order": [], "placeholders": {} }, - "pick_attachments": "Pick attachments", + "pick_attachments": "Chọn tệp đính kèm", "@pick_attachments": { "type": "text", "placeholders_order": [], @@ -356,7 +356,7 @@ "placeholders_order": [], "placeholders": {} }, - "moved_to_mailbox": "Moved to {destinationMailboxPath}", + "moved_to_mailbox": "Đã di chuyển tới {destinationMailboxPath}", "@moved_to_mailbox": { "type": "text", "placeholders_order": [ @@ -378,7 +378,7 @@ "placeholders_order": [], "placeholders": {} }, - "marked_star_multiple_item": "Marked star {count} item", + "marked_star_multiple_item": "Đã gắn sao {count} mục", "@marked_star_multiple_item": { "type": "text", "placeholders_order": [ @@ -388,7 +388,7 @@ "count": {} } }, - "marked_unstar_multiple_item": "Marked unstar {count} item", + "marked_unstar_multiple_item": "Đã gỡ sao {count} mục", "@marked_unstar_multiple_item": { "type": "text", "placeholders_order": [ @@ -398,25 +398,25 @@ "count": {} } }, - "search_mail": "Search mail", + "search_mail": "Tìm kiếm thư", "@search_mail": { "type": "text", "placeholders_order": [], "placeholders": {} }, - "prefix_suggestion_search": "Search for", + "prefix_suggestion_search": "Tìm kiếm", "@prefix_suggestion_search": { "type": "text", "placeholders_order": [], "placeholders": {} }, - "no_emails_matching_your_search": "No emails are matching your search", + "no_emails_matching_your_search": "Không có email phù hợp với tìm kiếm của bạn", "@no_emails_matching_your_search": { "type": "text", "placeholders_order": [], "placeholders": {} }, - "results": "Results", + "results": "Kết quả", "@results": { "type": "text", "placeholders_order": [], @@ -1968,7 +1968,7 @@ "placeholders_order": [], "placeholders": {} }, - "messageConfirmationDialogDeleteAllRecipientForward": "Bạn có muốn xoá tất cả email?", + "messageConfirmationDialogDeleteAllRecipientForward": "Bạn có chắc chắn muốn xóa những người nhận đó không? Thao tác này sẽ xóa họ khỏi danh sách.", "@messageConfirmationDialogDeleteAllRecipientForward": { "type": "text", "placeholders_order": [], @@ -2170,7 +2170,7 @@ "placeholders_order": [], "placeholders": {} }, - "foreground": "Foreground", + "foreground": "Vấn đề xung quanh", "@foreground": { "type": "text", "placeholders_order": [], @@ -2194,7 +2194,7 @@ "placeholders_order": [], "placeholders": {} }, - "titleForeground": "Foreground", + "titleForeground": "Vấn đề xung quanh", "@titleForeground": { "type": "text", "placeholders_order": [], @@ -2260,7 +2260,7 @@ "placeholders_order": [], "placeholders": {} }, - "keepLocalCopyForwardLabel": "Giữ bản sao", + "keepLocalCopyForwardLabel": "Lưu bản sao của email vào inbox", "@keepLocalCopyForwardLabel": { "type": "text", "placeholders_order": [], @@ -2345,5 +2345,617 @@ "type": "text", "placeholders_order": [], "placeholders": {} + }, + "unknownError": "Lỗi không xác định, xin hãy thử lại", + "@unknownError": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "skip": "Bỏ qua", + "@skip": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "profilesSettingExplanation": "Thông tin về bạn và các tùy chọn để quản lý.", + "@profilesSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "setDefaultIdentity": "Đặt làm định danh mặc định", + "@setDefaultIdentity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "identitiesSettingExplanation": "Chọn danh tính hoặc địa chỉ email mà bạn muốn sử dụng để gửi email", + "@identitiesSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "createNewIdentity": "Tạo danh tính mới", + "@createNewIdentity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "regards": "Trân trọng,", + "@regards": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "youHaveNewMessages": "Bạn có tin nhắn mới", + "@youHaveNewMessages": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "appTitlePushNotification": "Team Mail", + "@appTitlePushNotification": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "totalNewMessagePushNotification": "{count} email mới", + "@totalNewMessagePushNotification": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "privacyPolicy": "Chính sách bảo mật", + "@privacyPolicy": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "countNewSpamEmails": "Bạn có {count} email rác mới!", + "@countNewSpamEmails": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "showDetails": "Chi tiết", + "@showDetails": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "dismiss": "Bỏ qua", + "@dismiss": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEmailInYourCurrentMailbox": "Chúng tôi xin lỗi, không có email nào trong hộp thư hiện tại của bạn", + "@noEmailInYourCurrentMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "required": "yêu cầu", + "@required": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "noEmailMatchYourCurrentFilter": "Chúng tôi xin lỗi, không có email nào phù hợp với bộ lọc hiện tại của bạn.", + "@noEmailMatchYourCurrentFilter": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "reduceSomeFiltersAndTryAgain": "Hãy giảm bớt một số bộ lọc và thử lại", + "@reduceSomeFiltersAndTryAgain": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailure": "Không thể gửi tin nhắn của bạn.", + "@sendMessageFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailureWithSetErrorTypeTooLarge": "Không thể gửi tin nhắn của bạn, vì nó quá lớn.", + "@sendMessageFailureWithSetErrorTypeTooLarge": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailBoxes": "Hộp thư", + "@mailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "thisImageCannotBeAdded": "Không thể thêm hình ảnh này.", + "@thisImageCannotBeAdded": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "showMailbox": "Hiển thị hộp thư", + "@showMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "undo": "Hoàn tác", + "@undo": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "showAll": "Hiển thị tất cả", + "@showAll": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hintInputAutocompleteContact": "Nhập tên hoặc địa chỉ email", + "@hintInputAutocompleteContact": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleReadReceiptRequestNotificationMessage": "Yêu cầu xác nhận đọc", + "@titleReadReceiptRequestNotificationMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "no": "Không", + "@no": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageSendReceiptSuccess": "Xác nhận đã đọc được gửi.", + "@toastMessageSendReceiptSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageErrorNotSelectedFolderWhenCreateNewMailbox": "Bạn chưa chọn thư mục để lưu trữ", + "@toastMessageErrorNotSelectedFolderWhenCreateNewMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "newer": "Mới hơn", + "@newer": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "totalEmailSelected": "Bỏ chọn tất cả ({count})", + "@totalEmailSelected": { + "type": "text", + "placeholders_order": [ + "count" + ], + "placeholders": { + "count": {} + } + }, + "storageQuotas": "Dung lượng lưu trữ", + "@storageQuotas": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasUsed": "Đã sử dụng {used} GB trong tổng số {softLimit} GB", + "@textQuotasUsed": { + "type": "text", + "placeholders_order": [ + "used", + "softLimit" + ], + "placeholders": { + "used": {}, + "softLimit": {} + } + }, + "textQuotasRunningOutOfStorageTitle": "Dung lượng lưu trữ của bạn đang cạn kiệt ({progress}%).", + "@textQuotasRunningOutOfStorageTitle": { + "type": "text", + "placeholders_order": [ + "progress" + ], + "placeholders": { + "progress": {} + } + }, + "openInNewTab": "Mở trong tab mới", + "@openInNewTab": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "moveConversation": "Chuyển {numberOfConversation} cuộc trò chuyện", + "@moveConversation": { + "type": "text", + "placeholders_order": [ + "numberOfConversation" + ], + "placeholders": { + "numberOfConversation": {} + } + }, + "disableSpamReport": "Tắt báo cáo thư rác", + "@disableSpamReport": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "remove": "Xóa", + "@remove": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "incorrectEmailFormat": "Email không đúng định dạng", + "@incorrectEmailFormat": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "addRecipientButton": "Thêm người nhận", + "@addRecipientButton": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasRunningOutOfStorageContent": "Sắp tới bạn sẽ không thể gửi email trong Team Mail. Vui lòng dọn dẹp không gian lưu trữ hoặc nâng cấp dung lượng để sử dụng đầy đủ tính năng trong Team Mail.", + "@textQuotasRunningOutOfStorageContent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasOutOfStorage": "Hết dung lượng lưu trữ", + "@textQuotasOutOfStorage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasRunOutOfStorageTitle": "Bạn đã hết dung lượng lưu trữ", + "@textQuotasRunOutOfStorageTitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "page404": "Trang 404", + "@page404": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textQuotasRunOutOfStorageContent": "Bây giờ bạn tạm thời không thể gửi hoặc nhận email. Vui lòng giải phóng hoặc nâng cấp bộ nhớ của bạn để sử dụng các tính năng đầy đủ của Team Mail.", + "@textQuotasRunOutOfStorageContent": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subTitlePageNotFound": "Có thể trang đích của bạn đã biến mất hoặc thuộc về tài khoản khác.", + "@subTitlePageNotFound": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "quickCreatingRule": "Tạo một quy tắc với email này", + "@quickCreatingRule": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titlePageNotFound": "Xin lỗi, chúng tôi không thể tìm thấy trang", + "@titlePageNotFound": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "bcc_to": "Bcc", + "@bcc_to": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "personalFolders": "Thư mục cá nhân", + "@personalFolders": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "textBodySendReceiptToSender": "Tin nhắn đã được {receiver} đọc vào lúc {time}\n\nChủ đề: {subject}\n\nChú ý: Xác nhận đã đọc này chỉ xác nhận rằng tin nhắn đã hiển thị trên máy tính của người nhận. Không có bảo đảm rằng người nhận đã đọc hoặc hiểu nội dung của tin nhắn.", + "@textBodySendReceiptToSender": { + "type": "text", + "placeholders_order": [ + "receiver", + "subject", + "time" + ], + "placeholders": { + "receiver": {}, + "subject": {}, + "time": {} + } + }, + "createNewMailbox": "Tạo hộp thư mới", + "@createNewMailbox": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "reply_to": "Trả lời", + "@reply_to": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "html": "Html", + "@html": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "you_have_created_a_new_default_identity": "Bạn đã tạo một danh tính mặc định mới", + "@you_have_created_a_new_default_identity": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "selectParentFolder": "Chọn thư mục cha", + "@selectParentFolder": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "requestReadReceipt": "Yêu cầu xác nhận đọc", + "@requestReadReceipt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "appGridTittle": "Mở ứng dụng", + "@appGridTittle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageNotSupportMdnWhenSendReceipt": "Tài khoản của bạn không hỗ trợ tính năng MDN", + "@toastMessageNotSupportMdnWhenSendReceipt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "forwardingSettingExplanation": "Cho phép người nhận mới xem email đã gửi nếu họ không được bao gồm trong chuỗi email ban đầu.", + "@forwardingSettingExplanation": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subTitleReadReceiptRequestNotificationMessage": "Người gửi đã yêu cầu xác nhận đọc cho email này. Gửi xác nhận đọc?", + "@subTitleReadReceiptRequestNotificationMessage": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "older": "Cũ hơn", + "@older": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageCannotFoundEmailIdWhenSendReceipt": "Không tìm thấy email id", + "@toastMessageCannotFoundEmailIdWhenSendReceipt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "subjectSendReceiptToSender": "Đọc {subject}", + "@subjectSendReceiptToSender": { + "type": "text", + "placeholders_order": [ + "subject" + ], + "placeholders": { + "subject": {} + } + }, + "messageConfirmationDialogDeleteMultipleMailbox": "{numberOfMailbox} hộp thư và tất cả các thư mục con và các email bên trong sẽ bị xóa và không thể khôi phục được. Bạn có muốn tiếp tục xóa không?", + "@messageConfirmationDialogDeleteMultipleMailbox": { + "type": "text", + "placeholders_order": [ + "numberOfMailbox" + ], + "placeholders": { + "numberOfMailbox": {} + } + }, + "toastMessageCannotFoundIdentityWhenSendReceipt": "Không tìm thấy id danh tính", + "@toastMessageCannotFoundIdentityWhenSendReceipt": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "enableSpamReport": "Bật báo cáo Spam", + "@enableSpamReport": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "teamMailBoxes": "Hộp thư nhóm", + "@teamMailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sendMessageFailureWithSetErrorTypeOverQuota": "Không thể gửi tin nhắn của bạn, vì vượt quá giới hạn dung lượng.", + "@sendMessageFailureWithSetErrorTypeOverQuota": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailure": "Không thể lưu tin nhắn của bạn vào mục nháp.", + "@saveEmailAsDraftFailure": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailureWithSetErrorTypeOverQuota": "Không thể lưu tin nhắn của bạn dưới dạng bản nháp vì nó vượt quá giới hạn dung lượng.", + "@saveEmailAsDraftFailureWithSetErrorTypeOverQuota": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "saveEmailAsDraftFailureWithSetErrorTypeTooLarge": "Không thể lưu tin nhắn của bạn vào mục nháp, vì nó quá lớn.", + "@saveEmailAsDraftFailureWithSetErrorTypeTooLarge": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "hideMailBoxes": "Ẩn hộp thư", + "@hideMailBoxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMsgHideMailboxSuccess": "Hộp thư này đã bị ẩn khỏi hộp thư chính của bạn", + "@toastMsgHideMailboxSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "searchForMailboxes": "Tìm kiếm hộp thư", + "@searchForMailboxes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailboxVisibility": "Chế độ hiển thị hộp thư", + "@mailboxVisibility": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "mailboxVisibilitySubtitle": "Hiển thị/ẩn hộp thư của bạn, bao gồm hộp thư cá nhân và hộp thư nhóm của bạn.", + "@mailboxVisibilitySubtitle": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "toastMessageShowMailboxSuccess": "Hộp thư này đã được hiển thị trong hộp thư chính của bạn", + "@toastMessageShowMailboxSuccess": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "titleHeaderAttachment": "{count} Tệp đính kèm ({totalSize}):", + "@titleHeaderAttachment": { + "type": "text", + "placeholders_order": [ + "count", + "totalSize" + ], + "placeholders": { + "count": {}, + "totalSize": {} + } + }, + "yes": "Có", + "@yes": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "Bad credentials": "Thông tin đăng nhập không chính xác", + "@Bad credentials": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageArabic": "Tiếng Ả Rập", + "@languageArabic": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "languageItalian": "Tiếng Ý", + "@languageItalian": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "emptyListEmailForward": "Vui lòng nhập ít nhất một người nhận", + "@emptyListEmailForward": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "connectionError": "Lỗi kết nối", + "@connectionError": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "inboxMailboxDisplayName": "Hộp thư đến", + "@inboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "sentMailboxDisplayName": "Đã gửi", + "@sentMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "outboxMailboxDisplayName": "Hộp thư đi", + "@outboxMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "spamMailboxDisplayName": "Thư rác", + "@spamMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "draftsMailboxDisplayName": "Thư nháp", + "@draftsMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "trashMailboxDisplayName": "Thùng rác", + "@trashMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "templatesMailboxDisplayName": "Mẫu", + "@templatesMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} + }, + "archiveMailboxDisplayName": "Lưu trữ", + "@archiveMailboxDisplayName": { + "type": "text", + "placeholders_order": [], + "placeholders": {} } } diff --git a/lib/main.dart b/lib/main.dart index 738187d8dc..cd574cabca 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,12 @@ -import 'package:core/core.dart'; +import 'package:core/presentation/utils/theme_utils.dart'; +import 'package:core/utils/app_logger.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; +import 'package:tmail_ui_user/features/offline_mode/config/work_manager_config.dart'; +import 'package:tmail_ui_user/features/push_notification/presentation/notification/local_notification_manager.dart'; import 'package:tmail_ui_user/main/bindings/main_bindings.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; import 'package:tmail_ui_user/main/localizations/app_localizations_delegate.dart'; @@ -10,17 +14,29 @@ import 'package:tmail_ui_user/main/localizations/localization_service.dart'; import 'package:tmail_ui_user/main/pages/app_pages.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/utils/app_utils.dart'; +import 'package:url_strategy/url_strategy.dart'; import 'package:worker_manager/worker_manager.dart'; void main() async { initLogger(() async { WidgetsFlutterBinding.ensureInitialized(); ThemeUtils.setSystemLightUIStyle(); - await MainBindings().dependencies(); - await HiveCacheConfig().setUp(); + + await Future.wait([ + MainBindings().dependencies(), + HiveCacheConfig().setUp(), + Executor().warmUp(), + if (PlatformInfo.isMobile) + ... [ + WorkManagerConfig().initialize(), + LocalNotificationManager.instance.setUp(), + ], + AppUtils.loadEnvFile() + ]); await HiveCacheConfig.initializeEncryptionKey(); - await Executor().warmUp(); - await AppUtils.loadEnvFile(); + + setPathUrlStrategy(); + runApp(const TMailApp()); }); } @@ -59,7 +75,7 @@ class TMailApp extends StatelessWidget { } }, unknownRoute: AppPages.unknownRoutePage, - defaultTransition: Transition.fade, + defaultTransition: Transition.noTransition, initialRoute: AppRoutes.home, getPages: AppPages.pages); } diff --git a/lib/main/bindings/core/core_bindings.dart b/lib/main/bindings/core/core_bindings.dart index 3d53992eed..13be1151c6 100644 --- a/lib/main/bindings/core/core_bindings.dart +++ b/lib/main/bindings/core/core_bindings.dart @@ -1,9 +1,15 @@ -import 'package:core/core.dart'; +import 'package:core/data/utils/compress_file_utils.dart'; +import 'package:core/data/utils/device_manager.dart'; +import 'package:core/presentation/resources/image_paths.dart'; +import 'package:core/presentation/utils/app_toast.dart'; +import 'package:core/presentation/utils/responsive_utils.dart'; +import 'package:core/utils/config/app_config_loader.dart'; +import 'package:core/utils/file_utils.dart'; +import 'package:core/utils/platform_info.dart'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; +import 'package:tmail_ui_user/features/sending_queue/presentation/utils/sending_queue_isolate_manager.dart'; import 'package:tmail_ui_user/main/utils/email_receive_manager.dart'; import 'package:uuid/uuid.dart'; @@ -14,12 +20,11 @@ class CoreBindings extends Bindings { await _bindingSharePreference(); _bindingAppImagePaths(); _bindingResponsiveManager(); - _bindingKeyboardManager(); - _bindingTransformer(); _bindingToast(); _bindingDeviceManager(); _bindingReceivingSharingStream(); _bindingUtils(); + _bindingIsolate(); } void _bindingAppImagePaths() { @@ -34,16 +39,7 @@ class CoreBindings extends Bindings { await Get.putAsync(() async => await SharedPreferences.getInstance(), permanent: true); } - void _bindingKeyboardManager() { - Get.put(KeyboardUtils()); - } - - void _bindingTransformer() { - Get.put(HtmlAnalyzer()); - } - void _bindingToast() { - Get.put(FToast()); Get.put(AppToast()); } @@ -60,5 +56,12 @@ class CoreBindings extends Bindings { Get.put(const Uuid()); Get.put(CompressFileUtils()); Get.put(AppConfigLoader()); + Get.put(FileUtils()); + } + + void _bindingIsolate() { + if (PlatformInfo.isMobile) { + Get.put(SendingQueueIsolateManager()); + } } } \ No newline at end of file diff --git a/lib/main/bindings/credential/credential_bindings.dart b/lib/main/bindings/credential/credential_bindings.dart index 7f64e99dea..f4ca49ae9c 100644 --- a/lib/main/bindings/credential/credential_bindings.dart +++ b/lib/main/bindings/credential/credential_bindings.dart @@ -1,32 +1,105 @@ import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; +import 'package:tmail_ui_user/features/login/data/datasource/account_datasource.dart'; +import 'package:tmail_ui_user/features/login/data/datasource/authentication_datasource.dart'; +import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; +import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_datasource_impl.dart'; +import 'package:tmail_ui_user/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart'; +import 'package:tmail_ui_user/features/login/data/datasource_impl/hive_account_datasource_impl.dart'; +import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/authentication_info_cache_manager.dart'; +import 'package:tmail_ui_user/features/login/data/local/oidc_configuration_cache_manager.dart'; +import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; +import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart'; +import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; +import 'package:tmail_ui_user/features/login/data/repository/account_repository_impl.dart'; +import 'package:tmail_ui_user/features/login/data/repository/authentication_oidc_repository_impl.dart'; +import 'package:tmail_ui_user/features/login/data/repository/authentication_repository_impl.dart'; import 'package:tmail_ui_user/features/login/data/repository/credential_repository_impl.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/account_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_repository.dart'; import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/delete_authority_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/delete_credential_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_credential_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_token_oidc_interactor.dart'; +import 'package:tmail_ui_user/features/manage_account/domain/usecases/log_out_oidc_interactor.dart'; +import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; +import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; -class CredentialBindings extends Bindings { +class CredentialBindings extends InteractorsBindings { @override - void dependencies() { - bindingsRepositoryImpl(); - bindingsRepository(); - bindingsInteractor(); - } - void bindingsInteractor() { Get.put(GetCredentialInteractor(Get.find())); Get.put(DeleteCredentialInteractor(Get.find())); + Get.put(LogoutOidcInteractor( + Get.find(), + Get.find(), + )); + Get.put(DeleteAuthorityOidcInteractor( + Get.find(), + Get.find()) + ); + Get.put(GetStoredTokenOidcInteractor( + Get.find(), + Get.find(), + )); + Get.put(GetAuthenticatedAccountInteractor( + Get.find(), + Get.find(), + Get.find(), + )); + Get.put(AuthenticationInteractor( + Get.find(), + Get.find(), + Get.find() + )); + } + + @override + void bindingsDataSourceImpl() { + Get.put(HiveAccountDatasourceImpl( + Get.find(), + Get.find()) + ); + Get.put(AuthenticationDataSourceImpl()); + Get.put(AuthenticationOIDCDataSourceImpl( + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + )); } + @override + void bindingsDataSource() { + Get.put(Get.find()); + Get.put(Get.find()); + Get.put(Get.find()); + } + + @override void bindingsRepository() { Get.put(Get.find()); + Get.put(Get.find()); + Get.put(Get.find()); + Get.put(Get.find()); } + @override void bindingsRepositoryImpl() { Get.put(CredentialRepositoryImpl( - Get.find(), - Get.find())); + Get.find(), + Get.find() + )); + Get.put(AccountRepositoryImpl(Get.find())); + Get.put(AuthenticationRepositoryImpl(Get.find())); + Get.put(AuthenticationOIDCRepositoryImpl(Get.find())); } } \ No newline at end of file diff --git a/lib/main/bindings/local/local_bindings.dart b/lib/main/bindings/local/local_bindings.dart index 91cfcaed13..158b1367b9 100644 --- a/lib/main/bindings/local/local_bindings.dart +++ b/lib/main/bindings/local/local_bindings.dart @@ -1,18 +1,25 @@ +import 'package:core/utils/file_utils.dart'; import 'package:get/get.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:tmail_ui_user/features/caching/account_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/authentication_info_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/account_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/authentication_info_cache_client.dart'; import 'package:tmail_ui_user/features/caching/caching_manager.dart'; -import 'package:tmail_ui_user/features/caching/email_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/encryption_key_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/mailbox_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/recent_login_url_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/recent_login_username_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/recent_search_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/state_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/subscription_cache_client.dart'; -import 'package:tmail_ui_user/features/caching/token_oidc_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/email_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/encryption_key_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/fcm_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/hive_cache_version_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/mailbox_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/new_email_hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/opened_email_hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_login_url_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_login_username_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/recent_search_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/sending_email_hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/session_hive_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/subscription_cache_client.dart'; +import 'package:tmail_ui_user/features/caching/clients/token_oidc_cache_client.dart'; import 'package:tmail_ui_user/features/cleanup/data/local/recent_login_url_cache_manager.dart'; import 'package:tmail_ui_user/features/cleanup/data/local/recent_login_username_cache_manager.dart'; import 'package:tmail_ui_user/features/cleanup/data/local/recent_search_cache_manager.dart'; @@ -24,6 +31,11 @@ import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager import 'package:tmail_ui_user/features/mailbox/data/local/mailbox_cache_manager.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/data/local/share_preference_spam_report_data_source.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/new_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_manager.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/opened_email_cache_worker_queue.dart'; +import 'package:tmail_ui_user/features/offline_mode/manager/sending_email_cache_manager.dart'; import 'package:tmail_ui_user/features/push_notification/data/local/fcm_cache_manager.dart'; import 'package:tmail_ui_user/features/thread/data/local/email_cache_manager.dart'; import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; @@ -32,8 +44,9 @@ class LocalBindings extends Bindings { @override void dependencies() { - _bindingCaching(); _bindingException(); + _bindingCaching(); + _bindingWorkerQueue(); } void _bindingCaching() { @@ -59,15 +72,30 @@ class LocalBindings extends Bindings { Get.put(RecentLoginUsernameCacheClient()); Get.put(RecentLoginUsernameCacheManager(Get.find())); Get.put(FCMSubscriptionCacheClient()); - Get.put(FCMCacheManager(Get.find(),Get.find())); + Get.put(FcmCacheClient()); + Get.put(FCMCacheManager(Get.find(),Get.find())); + Get.put(HiveCacheVersionClient(Get.find(), Get.find())); + Get.put(NewEmailHiveCacheClient()); + Get.put(NewEmailCacheManager(Get.find(), Get.find())); + Get.put(OpenedEmailHiveCacheClient()); + Get.put(OpenedEmailCacheManager(Get.find(), Get.find())); + Get.put(SendingEmailHiveCacheClient()); + Get.put(SendingEmailCacheManager(Get.find())); + Get.put(SessionHiveCacheClient()); Get.put(CachingManager( Get.find(), Get.find(), Get.find(), Get.find(), Get.find(), - Get.find(), + Get.find(), Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), + Get.find(), )); Get.put(SharePreferenceSpamReportDataSource(Get.find())); } @@ -75,4 +103,9 @@ class LocalBindings extends Bindings { void _bindingException() { Get.put(CacheExceptionThrower()); } + + void _bindingWorkerQueue() { + Get.put(NewEmailCacheWorkerQueue()); + Get.put(OpenedEmailCacheWorkerQueue()); + } } \ No newline at end of file diff --git a/lib/main/bindings/main_bindings.dart b/lib/main/bindings/main_bindings.dart index 2035dc015c..702ba45bd6 100644 --- a/lib/main/bindings/main_bindings.dart +++ b/lib/main/bindings/main_bindings.dart @@ -15,7 +15,7 @@ class MainBindings extends Bindings { NetworkBindings().dependencies(); NetworkIsolateBindings().dependencies(); CredentialBindings().dependencies(); - NetWorkConnectionBindings().dependencies(); SessionBindings().dependencies(); + NetWorkConnectionBindings().dependencies(); } } \ No newline at end of file diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index 6d4576a972..fbe688ed93 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; @@ -7,7 +8,8 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:get/get.dart'; -import 'package:jmap_dart_client/http/http_client.dart' as JmapHttpClient; +import 'package:jmap_dart_client/http/http_client.dart'; +import 'package:tmail_ui_user/features/email/data/local/html_analyzer.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/email/data/network/mdn_api.dart'; import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; @@ -27,6 +29,7 @@ import 'package:tmail_ui_user/features/quotas/data/network/quotas_api.dart'; import 'package:tmail_ui_user/features/session/data/network/session_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; +import 'package:uuid/uuid.dart'; class NetworkBindings extends Bindings { @@ -35,6 +38,7 @@ class NetworkBindings extends Bindings { _bindingConnection(); _bindingDio(); _bindingApi(); + _bindingTransformer(); _bindingException(); } @@ -73,25 +77,27 @@ class NetworkBindings extends Bindings { } void _bindingApi() { - Get.put(JmapHttpClient.HttpClient(Get.find())); + Get.put(HttpClient(Get.find())); Get.put(DownloadClient(Get.find(), Get.find())); Get.put(DownloadManager(Get.find())); - Get.put(MailboxAPI(Get.find())); - Get.put(SessionAPI(Get.find())); - Get.put(ThreadAPI(Get.find())); + Get.put(MailboxAPI(Get.find())); + Get.put(SessionAPI(Get.find())); + Get.put(ThreadAPI(Get.find())); Get.put(EmailAPI( - Get.find(), + Get.find(), Get.find(), - Get.find())); - Get.put(RuleFilterAPI(Get.find())); - Get.put(VacationAPI(Get.find())); - Get.put(ContactAPI(Get.find())); - Get.put(IdentityAPI(Get.find())); - Get.put(MdnAPI(Get.find())); - Get.put(ForwardingAPI(Get.find())); - Get.put(QuotasAPI(Get.find())); - Get.put(FcmApi(Get.find())); - Get.put(SpamReportApi(Get.find())); + Get.find(), + Get.find(), + )); + Get.put(RuleFilterAPI(Get.find())); + Get.put(VacationAPI(Get.find())); + Get.put(ContactAPI(Get.find())); + Get.put(IdentityAPI(Get.find())); + Get.put(MdnAPI(Get.find())); + Get.put(ForwardingAPI(Get.find())); + Get.put(QuotasAPI(Get.find())); + Get.put(FcmApi(Get.find())); + Get.put(SpamReportApi(Get.find())); } void _bindingConnection() { @@ -101,4 +107,10 @@ class NetworkBindings extends Bindings { void _bindingException() { Get.put(RemoteExceptionThrower()); } + + void _bindingTransformer() { + Get.put(const HtmlEscape()); + Get.put(HtmlTransform(Get.find(), Get.find())); + Get.put(HtmlAnalyzer(Get.find())); + } } \ No newline at end of file diff --git a/lib/main/bindings/network/network_isolate_binding.dart b/lib/main/bindings/network/network_isolate_binding.dart index 2441b530fd..256354cedc 100644 --- a/lib/main/bindings/network/network_isolate_binding.dart +++ b/lib/main/bindings/network/network_isolate_binding.dart @@ -2,7 +2,7 @@ import 'package:core/core.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; -import 'package:jmap_dart_client/http/http_client.dart' as JmapHttpClient; +import 'package:jmap_dart_client/http/http_client.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.dart'; import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart'; @@ -12,6 +12,7 @@ import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_isolate_work import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_isolate_worker.dart'; import 'package:tmail_ui_user/main/bindings/network/binding_tag.dart'; +import 'package:uuid/uuid.dart'; import 'package:worker_manager/worker_manager.dart'; class NetworkIsolateBindings extends Bindings { @@ -44,14 +45,16 @@ class NetworkIsolateBindings extends Bindings { } void _bindingApi() { - final JmapHttpClient.HttpClient httpClient = Get.put(JmapHttpClient.HttpClient(Get.find(tag: BindingTag.isolateTag)), tag: BindingTag.isolateTag); + final httpClient = Get.put(HttpClient(Get.find(tag: BindingTag.isolateTag)), tag: BindingTag.isolateTag); Get.put(DownloadClient(Get.find(tag: BindingTag.isolateTag), Get.find()), tag: BindingTag.isolateTag); Get.put(DownloadManager(Get.find(tag: BindingTag.isolateTag)), tag: BindingTag.isolateTag); Get.put(ThreadAPI(httpClient), tag: BindingTag.isolateTag); Get.put(EmailAPI( httpClient, Get.find(tag: BindingTag.isolateTag), - Get.find(tag: BindingTag.isolateTag)), tag: BindingTag.isolateTag); + Get.find(tag: BindingTag.isolateTag), + Get.find() + ), tag: BindingTag.isolateTag); } void _bindingIsolateWorker() { diff --git a/lib/main/bindings/network_connection/network_connection_bindings.dart b/lib/main/bindings/network_connection/network_connection_bindings.dart index 14db154a99..b1067f2cab 100644 --- a/lib/main/bindings/network_connection/network_connection_bindings.dart +++ b/lib/main/bindings/network_connection/network_connection_bindings.dart @@ -1,32 +1,12 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/base/base_bindings.dart'; -import 'package:tmail_ui_user/features/network_status_handle/presentation/network_connnection_controller.dart'; +import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' + if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; -class NetWorkConnectionBindings extends BaseBindings { +class NetWorkConnectionBindings extends Bindings { @override - void bindingsController() { + void dependencies() { Get.put(NetworkConnectionController(Get.find())); } - - @override - void bindingsDataSource() { - } - - @override - void bindingsDataSourceImpl() { - } - - @override - void bindingsInteractor() { - } - - @override - void bindingsRepository() { - } - - @override - void bindingsRepositoryImpl() { - } } \ No newline at end of file diff --git a/lib/main/bindings/session/session_bindings.dart b/lib/main/bindings/session/session_bindings.dart index 801f1f44e4..9dbb08d56a 100644 --- a/lib/main/bindings/session/session_bindings.dart +++ b/lib/main/bindings/session/session_bindings.dart @@ -1,18 +1,18 @@ +import 'package:core/data/model/source_type/data_source_type.dart'; import 'package:get/get.dart'; -import 'package:tmail_ui_user/features/base/base_bindings.dart'; +import 'package:tmail_ui_user/features/base/interactors_bindings.dart'; +import 'package:tmail_ui_user/features/caching/clients/session_hive_cache_client.dart'; import 'package:tmail_ui_user/features/session/data/datasource/session_datasource.dart'; +import 'package:tmail_ui_user/features/session/data/datasource_impl/hive_session_datasource_impl.dart'; import 'package:tmail_ui_user/features/session/data/datasource_impl/session_datasource_impl.dart'; import 'package:tmail_ui_user/features/session/data/network/session_api.dart'; import 'package:tmail_ui_user/features/session/data/repository/session_repository_impl.dart'; import 'package:tmail_ui_user/features/session/domain/repository/session_repository.dart'; import 'package:tmail_ui_user/features/session/domain/usecases/get_session_interactor.dart'; +import 'package:tmail_ui_user/main/exceptions/cache_exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception_thrower.dart'; -class SessionBindings extends BaseBindings { - - @override - void bindingsController() { - } +class SessionBindings extends InteractorsBindings { @override void bindingsDataSource() { @@ -22,8 +22,11 @@ class SessionBindings extends BaseBindings { @override void bindingsDataSourceImpl() { Get.put(SessionDataSourceImpl( - Get.find(), - Get.find())); + Get.find(), + Get.find())); + Get.put(HiveSessionDataSourceImpl( + Get.find(), + Get.find())); } @override @@ -38,6 +41,9 @@ class SessionBindings extends BaseBindings { @override void bindingsRepositoryImpl() { - Get.put(SessionRepositoryImpl(Get.find())); + Get.put(SessionRepositoryImpl({ + DataSourceType.network: Get.find(), + DataSourceType.hiveCache: Get.find(), + })); } } \ No newline at end of file diff --git a/lib/main/error/capability_validator.dart b/lib/main/error/capability_validator.dart index 51d5b71942..abf09b54ea 100644 --- a/lib/main/error/capability_validator.dart +++ b/lib/main/error/capability_validator.dart @@ -1,3 +1,4 @@ +import 'package:core/utils/app_logger.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; @@ -23,4 +24,44 @@ void requireCapability(Session session, AccountId accountId, List { + + bool isSupported(Session session, AccountId accountId) { + try { + requireCapability(session, accountId, this); + return true; + } catch (error) { + logError('ListCapabilityIdentifierExtension::isSupported(): $error'); + return false; + } + } +} + +extension CapabilityIdentifierExtension on CapabilityIdentifier { + + bool isSupported(Session session, AccountId accountId) { + try { + requireCapability(session, accountId, [this]); + return true; + } catch (error) { + logError('CapabilityIdentifierExtension::isSupported(): $error'); + return false; + } + } +} + +extension CapabilityIdentifierSetExtension on Set { + + Set toCapabilitiesSupportTeamMailboxes(Session session, AccountId accountId) { + try { + requireCapability(session, accountId, [CapabilityIdentifier.jmapTeamMailboxes]); + add(CapabilityIdentifier.jmapTeamMailboxes); + return this; + } catch (error) { + logError('CapabilityIdentifierExtension::toCapabilitiesSupportTeamMailboxes(): $error'); + return this; + } + } } \ No newline at end of file diff --git a/lib/main/exceptions/cache_exception_thrower.dart b/lib/main/exceptions/cache_exception_thrower.dart index 01e0d18efd..488a4742a5 100644 --- a/lib/main/exceptions/cache_exception_thrower.dart +++ b/lib/main/exceptions/cache_exception_thrower.dart @@ -1,9 +1,11 @@ +import 'package:core/utils/app_logger.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; class CacheExceptionThrower extends ExceptionThrower { @override - void throwException(dynamic error) { + throwException(dynamic error, dynamic stackTrace) { + logError('CacheExceptionThrower::throwException():error: $error | stackTrace: $stackTrace'); throw error; } } \ No newline at end of file diff --git a/lib/main/exceptions/exception_thrower.dart b/lib/main/exceptions/exception_thrower.dart index 0f71f88e0c..175b85a665 100644 --- a/lib/main/exceptions/exception_thrower.dart +++ b/lib/main/exceptions/exception_thrower.dart @@ -1,4 +1,4 @@ abstract class ExceptionThrower { - void throwException(dynamic error); + throwException(dynamic error, dynamic stackTrace); } diff --git a/lib/main/exceptions/remote_exception.dart b/lib/main/exceptions/remote_exception.dart index fa7452c93f..69a8732f8b 100644 --- a/lib/main/exceptions/remote_exception.dart +++ b/lib/main/exceptions/remote_exception.dart @@ -4,8 +4,12 @@ import 'package:jmap_dart_client/jmap/core/error/error_type.dart'; import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; abstract class RemoteException with EquatableMixin implements Exception { - static const connectError = 'Connect error'; + static const connectionTimeout = 'Connection Timeout'; + static const connectionError = 'Connection error'; + static const internalServerError = 'Internal Server Error'; static const noNetworkError = 'No network error'; + static const badCredentials = 'Bad credentials'; + static const socketException = 'Socket exception'; final String? message; final int? code; @@ -13,18 +17,46 @@ abstract class RemoteException with EquatableMixin implements Exception { const RemoteException({this.code, this.message}); } +class BadCredentialsException extends RemoteException { + const BadCredentialsException() : super(message: RemoteException.badCredentials); + + @override + List get props => []; +} + class UnknownError extends RemoteException { const UnknownError({int? code, String? message}) : super(code: code, message: message); @override - List get props => []; + List get props => [code, message]; +} + +class ConnectionError extends RemoteException { + const ConnectionError({String? message}) : super(message: message ?? RemoteException.connectionError); + + @override + List get props => [code, message]; +} + +class ConnectionTimeout extends RemoteException { + const ConnectionTimeout({String? message}) : super(message: message ?? RemoteException.connectionTimeout); + + @override + List get props => [code, message]; +} + +class SocketError extends RemoteException { + const SocketError() : super(message: RemoteException.socketException); + + @override + List get props => [code, message]; } -class ConnectError extends RemoteException { - const ConnectError() : super(message: RemoteException.connectError); +class InternalServerError extends RemoteException { + const InternalServerError() : super(message: RemoteException.internalServerError); @override - List get props => []; + List get props => [code, message]; } class MethodLevelErrors extends RemoteException { @@ -36,7 +68,7 @@ class MethodLevelErrors extends RemoteException { ) : super(message: message); @override - List get props => [type, message]; + List get props => [type, code, message]; } class CannotCalculateChangesMethodResponseException extends MethodLevelErrors { @@ -47,5 +79,5 @@ class NoNetworkError extends RemoteException { const NoNetworkError() : super(message: RemoteException.noNetworkError); @override - List get props => [message]; + List get props => [code, message]; } \ No newline at end of file diff --git a/lib/main/exceptions/remote_exception_thrower.dart b/lib/main/exceptions/remote_exception_thrower.dart index 9dc1702cf4..6f1e6dd8a5 100644 --- a/lib/main/exceptions/remote_exception_thrower.dart +++ b/lib/main/exceptions/remote_exception_thrower.dart @@ -1,9 +1,13 @@ +import 'dart:io'; + import 'package:core/utils/app_logger.dart'; import 'package:dio/dio.dart'; +import 'package:get/get_connect/http/src/status/http_status.dart'; import 'package:jmap_dart_client/jmap/core/error/method/error_method_response.dart'; import 'package:jmap_dart_client/jmap/core/error/method/exception/error_method_response_exception.dart'; import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart'; -import 'package:tmail_ui_user/features/network_status_handle/presentation/network_connnection_controller.dart'; +import 'package:tmail_ui_user/features/network_connection/presentation/network_connection_controller.dart' + if (dart.library.html) 'package:tmail_ui_user/features/network_connection/presentation/web_network_connection_controller.dart'; import 'package:tmail_ui_user/main/exceptions/exception_thrower.dart'; import 'package:tmail_ui_user/main/exceptions/remote_exception.dart'; import 'package:tmail_ui_user/main/routes/route_navigation.dart'; @@ -11,40 +15,55 @@ import 'package:tmail_ui_user/main/routes/route_navigation.dart'; class RemoteExceptionThrower extends ExceptionThrower { @override - void throwException(dynamic error) { - logError('RemoteExceptionThrower::throwException():error: $error'); + throwException(dynamic error, dynamic stackTrace) { + logError('RemoteExceptionThrower::throwException():error: $error | stackTrace: $stackTrace'); final networkConnectionController = getBinding(); - if (networkConnectionController != null) { - log('RemoteExceptionThrower::throwException(): CONNECTION: ${networkConnectionController.connectivityResult.value}'); - if (!networkConnectionController.isNetworkConnectionAvailable()) { - throw const NoNetworkError(); - } - } - - if (error is DioError) { - switch (error.type) { - case DioErrorType.connectTimeout: - throw const ConnectError(); - default: - if (error.response?.statusCode == 502) { + if (networkConnectionController?.isNetworkConnectionAvailable() == false) { + logError('RemoteExceptionThrower::throwException():isNetworkConnectionAvailable'); + throw const NoNetworkError(); + } else { + if (error is DioError) { + logError('RemoteExceptionThrower::throwException():type: ${error.type} | response: ${error.response} | error: ${error.error}'); + if (error.response != null) { + if (error.response!.statusCode == HttpStatus.internalServerError) { + throw const InternalServerError(); + } else if (error.response!.statusCode == HttpStatus.badGateway) { throw BadGateway(); + } else if (error.response!.statusCode == HttpStatus.unauthorized) { + throw const BadCredentialsException(); } else { throw UnknownError( - code: error.response?.statusCode, - message: error.response?.statusMessage); + code: error.response!.statusCode, + message: error.response!.statusMessage); } - } - } else if (error is ErrorMethodResponseException) { - final errorResponse = error.errorResponse as ErrorMethodResponse; - if (errorResponse is CannotCalculateChangesMethodResponse) { - throw CannotCalculateChangesMethodResponseException(); + } else { + switch (error.type) { + case DioErrorType.connectionTimeout: + throw ConnectionTimeout(message: error.message); + case DioErrorType.connectionError: + throw ConnectionError(message: error.message); + default: + if (error.error is SocketException) { + throw const SocketError(); + } else if (error.error != null) { + throw UnknownError(message: error.error!.toString()); + } else { + throw const UnknownError(); + } + } + } + } else if (error is ErrorMethodResponseException) { + final errorResponse = error.errorResponse as ErrorMethodResponse; + if (errorResponse is CannotCalculateChangesMethodResponse) { + throw CannotCalculateChangesMethodResponseException(); + } else { + throw MethodLevelErrors( + errorResponse.type, + message: errorResponse.description); + } } else { - throw MethodLevelErrors( - errorResponse.type, - message: errorResponse.description); + throw error; } - } else { - throw error; } } } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 9b4e9c70e1..24e191e7fe 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -36,6 +36,10 @@ class AppLocalizations { String get loginInputCredentialMessage { return Intl.message('Enter your credentials to sign in', name: 'loginInputCredentialMessage'); } + + String get badCredentials { + return Intl.message('Bad credentials'); + } String get next { return Intl.message('Next', name: 'next'); @@ -117,10 +121,10 @@ class AppLocalizations { ); } - String get personalMailboxes { + String get personalFolders { return Intl.message( - 'Personal mailboxes', - name: 'personalMailboxes', + 'Personal folders', + name: 'personalFolders', ); } @@ -1188,7 +1192,7 @@ class AppLocalizations { String get profilesSettingExplanation { return Intl.message( - 'Info about you, and options to manage it', + 'Info about you, and options to manage it.', name: 'profilesSettingExplanation' ); } @@ -1246,12 +1250,6 @@ class AppLocalizations { name: 'signature'); } - String get plain_text { - return Intl.message( - 'Plain text', - name: 'plain_text'); - } - String get html_template { return Intl.message( 'Html template', @@ -1515,13 +1513,6 @@ class AppLocalizations { ); } - String get allMails { - return Intl.message( - 'All mails', - name: 'allMails', - ); - } - String get allTime { return Intl.message( 'All time', @@ -1606,6 +1597,18 @@ class AppLocalizations { name: 'languageRussian'); } + String get languageArabic { + return Intl.message( + 'Arabic', + name: 'languageArabic'); + } + + String get languageItalian { + return Intl.message( + 'Italian', + name: 'languageItalian'); + } + String get messageDialogSendEmailUploadingAttachment { return Intl.message( 'Your message could not be sent because it uploading attachment', @@ -1718,12 +1721,6 @@ class AppLocalizations { name: 'formatTextBackgroundColor'); } - String get codeView { - return Intl.message( - 'Code view', - name: 'codeView'); - } - String get headerStyle { return Intl.message( 'Style', @@ -2544,29 +2541,6 @@ class AppLocalizations { ); } - String textQuotasUsed(double used, double softLimit) { - return Intl.message( - '$used GB of $softLimit GB Used', - name: 'textQuotasUsed', - args: [used, softLimit], - ); - } - - String textQuotasRunningOutOfStorageTitle(double progress) { - return Intl.message( - 'You are running out of storage ($progress%).', - name: 'textQuotasRunningOutOfStorageTitle', - args: [progress], - ); - } - - String get textQuotasRunningOutOfStorageContent { - return Intl.message( - "Soon you won't be able to email in Team Mail. Please clean your storage or upgrade your storage to get full features in Team Mail.", - name: 'textQuotasRunningOutOfStorageContent', - ); - } - String get textQuotasOutOfStorage { return Intl.message( 'Out of storage', @@ -2574,20 +2548,6 @@ class AppLocalizations { ); } - String get textQuotasRunOutOfStorageTitle { - return Intl.message( - 'You have run out of storage space', - name: 'textQuotasRunOutOfStorageTitle', - ); - } - - String get textQuotasRunOutOfStorageContent { - return Intl.message( - "Now you temporarily can't send or get an email. Please free up or upgrade your storage to get the full features of Team Mail.", - name: 'textQuotasRunOutOfStorageContent', - ); - } - String get quickCreatingRule { return Intl.message( 'Create a rule with this email', @@ -2653,7 +2613,7 @@ class AppLocalizations { ); } - String countNewSpamEmails(int count,) { + String countNewSpamEmails(String count) { return Intl.message( 'You have $count new spam emails!', name: 'countNewSpamEmails', @@ -2818,4 +2778,587 @@ class AppLocalizations { name: 'mailboxVisibilitySubtitle', ); } + + String get emptyListEmailForward { + return Intl.message( + 'Please input at least one recipient', + name: 'emptyListEmailForward'); + } + + String get forwardedMessage { + return Intl.message( + 'Forwarded message', + name: 'forwardedMessage'); + } + + String get repliedMessage { + return Intl.message( + 'Replied message', + name: 'repliedMessage'); + } + + String get repliedAndForwardedMessage { + return Intl.message( + 'Replied and Forwarded message', + name: 'repliedAndForwardedMessage'); + } + + String get emptyTrash { + return Intl.message( + 'Empty Trash', + name: 'emptyTrash'); + } + + String get emptyTrashMessageDialog { + return Intl.message( + 'You are about to permanently delete all items in Trash . Do you want to continue?', + name: 'emptyTrashMessageDialog'); + } + + String get cannotSelectThisImage { + return Intl.message( + 'Cannot select this image.', + name: 'cannotSelectThisImage'); + } + + String get messageHasBeenSavedToTheSendingQueue { + return Intl.message( + 'Message has been saved to the sending queue.', + name: 'messageHasBeenSavedToTheSendingQueue', + ); + } + + String get sendingQueue { + return Intl.message( + 'Sending queue', + name: 'sendingQueue' + ); + } + + String get bannerMessageSendingQueueView { + return Intl.message( + 'Messages in Sending queue mailbox will be sent or scheduled when online.', + name: 'bannerMessageSendingQueueView' + ); + } + + String get proceed { + return Intl.message( + 'Proceed', + name: 'proceed' + ); + } + + String get youAreInOfflineMode { + return Intl.message( + 'You\'re in offline mode', + name: 'youAreInOfflineMode' + ); + } + + String get messageDialogWhenStoreSendingEmailFirst { + return Intl.message( + 'Fortunately, you can still', + name: 'messageDialogWhenStoreSendingEmailFirst' + ); + } + + String get messageDialogWhenStoreSendingEmailSecond { + return Intl.message( + ' send, reply, or forward ', + name: 'messageDialogWhenStoreSendingEmailSecond' + ); + } + + String get messageDialogWhenStoreSendingEmailThird { + return Intl.message( + 'emails. They will be delivered when you connect to the internet. To edit these emails before sending, go to the ', + name: 'messageDialogWhenStoreSendingEmailThird' + ); + } + + String get messageDialogWhenStoreSendingEmailTail { + return Intl.message( + ' mailbox.', + name: 'messageDialogWhenStoreSendingEmailTail' + ); + } + + String titleRecipientSendingEmail(String recipients) { + return Intl.message( + 'To: $recipients', + name: 'titleRecipientSendingEmail', + args: [recipients]); + } + + String get openMailboxMenu { + return Intl.message( + 'Open mailbox menu', + name: 'openMailboxMenu' + ); + } + + String get messageHasBeenSentSuccessfully { + return Intl.message( + 'Message has been sent successfully.', + name: 'messageHasBeenSentSuccessfully', + ); + } + + String get deleteOfflineEmail { + return Intl.message( + 'Delete offline email', + name: 'deleteOfflineEmail' + ); + } + + String get messageDialogDeleteSendingEmail { + return Intl.message( + 'Deleting an offline email will erase its content permanently. You won\'t be able to undo this action or recover the email from the Trash mailbox.', + name: 'messageDialogDeleteSendingEmail' + ); + } + + String get messageHaveBeenDeletedSuccessfully { + return Intl.message( + 'Messages have been deleted successfully', + name: 'messageHaveBeenDeletedSuccessfully', + ); + } + + String get delivering { + return Intl.message( + 'Delivering', + name: 'delivering', + ); + } + + String get error { + return Intl.message( + 'Error', + name: 'error', + ); + } + + String get connectedToTheInternet { + return Intl.message( + 'Connected to the internet', + name: 'connectedToTheInternet' + ); + } + + String get resend { + return Intl.message( + 'Resend', + name: 'resend'); + } + + String get messagesHaveBeenResent { + return Intl.message( + 'Messages have been resent', + name: 'messagesHaveBeenResent'); + } + + String get connectionError { + return Intl.message( + 'Connection error', + name: 'connectionError' + ); + } + + String get inboxMailboxDisplayName { + return Intl.message( + 'Inbox', + name: 'inboxMailboxDisplayName', + ); + } + + String get sentMailboxDisplayName { + return Intl.message( + 'Sent', + name: 'sentMailboxDisplayName', + ); + } + + String get outboxMailboxDisplayName { + return Intl.message( + 'Outbox', + name: 'outboxMailboxDisplayName', + ); + } + + String get spamMailboxDisplayName { + return Intl.message( + 'Spam', + name: 'spamMailboxDisplayName', + ); + } + + String get draftsMailboxDisplayName { + return Intl.message( + 'Drafts', + name: 'draftsMailboxDisplayName', + ); + } + + String get trashMailboxDisplayName { + return Intl.message( + 'Trash', + name: 'trashMailboxDisplayName', + ); + } + + String get templatesMailboxDisplayName { + return Intl.message( + 'Templates', + name: 'templatesMailboxDisplayName', + ); + } + + String get archiveMailboxDisplayName { + return Intl.message( + 'Archive', + name: 'archiveMailboxDisplayName', + ); + } + + String pleaseChooseAnImageSizeCorrectly(int maxSize) { + return Intl.message( + 'Please choose an image size <= ${maxSize}KB', + name: 'pleaseChooseAnImageSizeCorrectly', + args: [maxSize]); + } + + String get messageEventActionBannerOrganizerInvited { + return Intl.message( + ' has invited you in to a meeting', + name: 'messageEventActionBannerOrganizerInvited'); + } + + String get messageEventActionBannerOrganizerUpdated { + return Intl.message( + ' has updated a meeting', + name: 'messageEventActionBannerOrganizerUpdated'); + } + + String get messageEventActionBannerOrganizerCanceled { + return Intl.message( + ' has canceled a meeting', + name: 'messageEventActionBannerOrganizerCanceled'); + } + + String get subMessageEventActionBannerUpdated { + return Intl.message( + '"The time has been updated to better suit all of you"', + name: 'subMessageEventActionBannerUpdated'); + } + + String get subMessageEventActionBannerCanceled { + return Intl.message( + '"We are canceling the event due to bad weather."', + name: 'subMessageEventActionBannerCanceled'); + } + + String get anAttendee { + return Intl.message( + 'An attendee', + name: 'anAttendee'); + } + + String get you { + return Intl.message( + 'You', + name: 'you'); + } + + String get messageEventActionBannerAttendeeAccepted { + return Intl.message( + ' has accepted this invitation', + name: 'messageEventActionBannerAttendeeAccepted'); + } + + String get messageEventActionBannerAttendeeTentative { + return Intl.message( + ' has replied "Maybe" to this invitation', + name: 'messageEventActionBannerAttendeeTentative'); + } + + String get messageEventActionBannerAttendeeDeclined { + return Intl.message( + ' has declined this invitation', + name: 'messageEventActionBannerAttendeeDeclined'); + } + + String get messageEventActionBannerAttendeeCounter { + return Intl.message( + ' has proposed changes to the event', + name: 'messageEventActionBannerAttendeeCounter'); + } + + String get messageEventActionBannerAttendeeCounterDeclined { + return Intl.message( + 'Your counter proposal was declined', + name: 'messageEventActionBannerAttendeeCounterDeclined'); + } + + String get invitationMessageCalendarInformation { + return Intl.message( + ' has invited you in to a meeting:', + name: 'invitationMessageCalendarInformation'); + } + + String get when { + return Intl.message( + 'When', + name: 'when'); + } + + String get where { + return Intl.message( + 'Where', + name: 'where'); + } + + String get who { + return Intl.message( + 'Who', + name: 'who'); + } + + String get organizer { + return Intl.message( + 'Organizer', + name: 'organizer'); + } + + String get time { + return Intl.message( + 'Time', + name: 'time', + ); + } + + String get location { + return Intl.message( + 'Location', + name: 'location'); + } + + String get attendees { + return Intl.message( + 'Attendees', + name: 'attendees'); + } + + String get seeAllAttendees { + return Intl.message( + 'See all attendees', + name: 'seeAllAttendees'); + } + + String get link { + return Intl.message( + 'Link', + name: 'link'); + } + + String get deleteAllSpamEmails { + return Intl.message( + 'Delete all spam emails', + name: 'deleteAllSpamEmails'); + } + + String get emptySpamFolder { + return Intl.message( + 'Empty Spam folder', + name: 'emptySpamFolder'); + } + + String get emptySpamMessageDialog { + return Intl.message( + 'You are about to permanently delete all items in Spam . Do you want to continue?', + name: 'emptySpamMessageDialog'); + } + + String get bannerDeleteAllSpamEmailsMessage { + return Intl.message( + 'All messages in Spam will be deleted if you reach limited storage.', + name: 'bannerDeleteAllSpamEmailsMessage'); + } + + String get deleteAllSpamEmailsNow { + return Intl.message( + 'Delete all spam emails now', + name: 'deleteAllSpamEmailsNow'); + } + + String quotaStateLabel(String used, String limit) { + return Intl.message( + '$used of $limit Used', + name: 'quotaStateLabel', + args: [used, limit], + ); + } + + String get quotaErrorBannerTitle { + return Intl.message( + 'You have run out of storage space', + name: 'quotaErrorBannerTitle' + ); + } + + String get quotaWarningBannerTitle { + return Intl.message( + 'You are running out of storage (90%).', + name: 'quotaWarningBannerTitle' + ); + } + + String get quotaWarningBannerMessage { + return Intl.message( + 'Soon you won\'t be able to email in Tmail. Please clean your storage or upgrade your storage to get full features in Tmail.', + name: 'quotaWarningBannerMessage' + ); + } + + String get quotaErrorBannerMessage { + return Intl.message( + 'Soon you won\'t be able to email in Tmail. Please clean your storage or upgrade your storage to get full features in Tmail.', + name: 'quotaErrorBannerMessage' + ); + } + + String createFolderSuccessfullyMessage(String folderName) { + return Intl.message( + 'You successfully created $folderName folder', + name: 'createFolderSuccessfullyMessage', + args: [folderName] + ); + } + + String get folderCreatedTitle { + return Intl.message( + 'Your folder is just created', + name: 'folderCreatedTitle'); + } + + String get folderCreatedMessage { + return Intl.message( + 'To begin using this mailbox, you should add some rules to organize all of your mail in your own way.', + name: 'folderCreatedMessage'); + } + + String get createFilters { + return Intl.message( + 'Create filters', + name: 'createFilters'); + } + + + String get maybe { + return Intl.message( + 'Maybe', + name: 'maybe'); + } + + String get enterASubject { + return Intl.message( + 'Enter a subject', + name: 'enterASubject', + ); + } + + String get enterSomeSuggestions { + return Intl.message( + 'Enter some suggestions', + name: 'enterSomeSuggestions', + ); + } + + String markedSingleMessageToast(String action) { + return Intl.message( + 'Message has been marked as $action', + name: 'markedSingleMessageToast', + args: [action] + ); + } + + String get empty { + return Intl.message( + 'Empty', + name: 'empty', + ); + } + + String get clearFolder { + return Intl.message( + 'Clear folder', + name: 'clearFolder', + ); + } + + String messageEmptyFolderDialog(String folder) { + return Intl.message( + 'The messages in $folder folder will be permanently deleted and you will not be able to restore them', + name: 'messageEmptyFolderDialog', + args: [folder] + ); + } + + String get addCondition { + return Intl.message( + 'Add condition', + name: 'addCondition', + ); + } + + String get formattingOptions { + return Intl.message( + 'Formatting options', + name: 'formattingOptions' + ); + } + + String get embedCode { + return Intl.message( + 'Embed code', + name: 'embedCode' + ); + } + + String showMoreAttachment(int count) { + return Intl.message( + 'Show more (+$count)', + name: 'showMoreAttachment', + args: [count] + ); + } + + String get saveAsDraft { + return Intl.message( + 'Save as draft', + name: 'saveAsDraft', + ); + } + + String get dropFileHereToAttachThem { + return Intl.message( + 'Drop file here to attach them', + name: 'dropFileHereToAttachThem', + ); + } + + String get canceled { + return Intl.message( + 'Canceled', + name: 'canceled', + ); + } + + String get newSubfolder { + return Intl.message( + 'New subfolder', + name: 'newSubfolder', + ); + } } \ No newline at end of file diff --git a/lib/main/localizations/language_code_constants.dart b/lib/main/localizations/language_code_constants.dart new file mode 100644 index 0000000000..7b84b16be2 --- /dev/null +++ b/lib/main/localizations/language_code_constants.dart @@ -0,0 +1,9 @@ + +class LanguageCodeConstants { + static const String english = 'en'; + static const String french = 'fr'; + static const String vietnamese = 'vi'; + static const String italian = 'it'; + static const String russian = 'ru'; + static const String arabic = 'ar'; +} diff --git a/lib/main/localizations/localization_service.dart b/lib/main/localizations/localization_service.dart index 3f2b195d04..9d17d32bf4 100644 --- a/lib/main/localizations/localization_service.dart +++ b/lib/main/localizations/localization_service.dart @@ -4,24 +4,29 @@ import 'dart:ui'; import 'package:core/utils/app_logger.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/manage_account/data/local/language_cache_manager.dart'; +import 'package:tmail_ui_user/main/localizations/language_code_constants.dart'; class LocalizationService extends Translations { - static const defaultLocale = Locale('en', 'US'); - static const fallbackLocale = Locale('en', 'US'); + static const defaultLocale = Locale(LanguageCodeConstants.english, 'US'); + static const fallbackLocale = Locale(LanguageCodeConstants.english, 'US'); static final supportedLanguageCodes = [ - 'fr', - 'en', - 'vi', - 'ru' + LanguageCodeConstants.french, + LanguageCodeConstants.english, + LanguageCodeConstants.vietnamese, + LanguageCodeConstants.russian, + LanguageCodeConstants.arabic, + LanguageCodeConstants.italian ]; static const List supportedLocales = [ - Locale('fr', 'FR'), - Locale('en', 'US'), - Locale('vi', 'VN'), - Locale('ru', 'RU') + Locale(LanguageCodeConstants.french, 'FR'), + Locale(LanguageCodeConstants.english, 'US'), + Locale(LanguageCodeConstants.vietnamese, 'VN'), + Locale(LanguageCodeConstants.russian, 'RU'), + Locale(LanguageCodeConstants.arabic, 'TN'), + Locale(LanguageCodeConstants.italian, 'IT') ]; static final locale = _getLocaleFromLanguage(); diff --git a/lib/main/pages/app_pages.dart b/lib/main/pages/app_pages.dart index c5a63f649b..99455f6439 100644 --- a/lib/main/pages/app_pages.dart +++ b/lib/main/pages/app_pages.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:core/core.dart'; import 'package:get/get.dart'; import 'package:get/get_navigation/src/routes/get_route.dart'; @@ -9,6 +11,8 @@ import 'package:tmail_ui_user/features/destination_picker/presentation/destinati import 'package:tmail_ui_user/features/home/presentation/home_bindings.dart'; import 'package:tmail_ui_user/features/home/presentation/home_view.dart'; import 'package:tmail_ui_user/features/contact/presentation/contact_view.dart' deferred as contact_view; +import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_bindings.dart'; +import 'package:tmail_ui_user/features/mailto/presentation/mailto_url_bindings.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/rules_filter_creator_bindings.dart'; import 'package:tmail_ui_user/features/rules_filter_creator/presentation/rules_filter_creator_view.dart' deferred as rules_filter_creator; import 'package:tmail_ui_user/features/login/presentation/login_bindings.dart'; @@ -16,6 +20,7 @@ import 'package:tmail_ui_user/features/login/presentation/login_view.dart' if (dart.library.html) 'package:tmail_ui_user/features/login/presentation/login_view_web.dart' deferred as login; import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_bindings.dart'; import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_view.dart' deferred as mailbox_creator; +import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_view.dart' deferred as identity_creator; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/bindings/mailbox_dashboard_bindings.dart'; import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_view.dart' if (dart.library.html) 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_view_web.dart' deferred as mailbox_dashboard; @@ -28,6 +33,7 @@ import 'package:tmail_ui_user/features/unknown_route_page/unknown_route_page_vie import 'package:tmail_ui_user/main/pages/deferred_widget.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/features/search/mailbox/presentation/search_mailbox_view.dart' deferred as search_mailbox_view; +import 'package:tmail_ui_user/features/mailto/presentation/mailto_url_view.dart' deferred as mailto_url_view; class AppPages { static final pages = [ @@ -53,16 +59,33 @@ class AppPages { name: AppRoutes.dashboardWithParameter, page: () => DeferredWidget( mailbox_dashboard.loadLibrary, - () => mailbox_dashboard.MailboxDashBoardView()), - binding: MailboxDashBoardBindings()), + () => mailbox_dashboard.MailboxDashBoardView() + ), + binding: MailboxDashBoardBindings() + ), + GetPage( + name: AppRoutes.mailtoURL, + page: () => DeferredWidget( + mailto_url_view.loadLibrary, + () => mailto_url_view.MailtoUrlView() + ), + binding: MailtoUrlBindings() + ), GetPage( name: AppRoutes.settings, page: () => DeferredWidget( manage_account_dashboard.loadLibrary, () => manage_account_dashboard.ManageAccountDashBoardView()), binding: ManageAccountDashBoardBindings()), + GetPage( + name: AppRoutes.contact, + opaque: false, + page: () => DeferredWidget( + contact_view.loadLibrary, + () => contact_view.ContactView()), + binding: ContactBindings()), unknownRoutePage, - if (!BuildUtils.isWeb) + if (PlatformInfo.isMobile) ...[ GetPage( name: AppRoutes.composer, @@ -85,13 +108,6 @@ class AppPages { mailbox_creator.loadLibrary, () => mailbox_creator.MailboxCreatorView()), binding: MailboxCreatorBindings()), - GetPage( - name: AppRoutes.contact, - opaque: false, - page: () => DeferredWidget( - contact_view.loadLibrary, - () => contact_view.ContactView()), - binding: ContactBindings()), GetPage( name: AppRoutes.rulesFilterCreator, opaque: false, @@ -107,6 +123,14 @@ class AppPages { () => search_mailbox_view.SearchMailboxView() ), binding: SearchMailboxBindings()), + GetPage( + name: AppRoutes.identityCreator, + opaque: Platform.isAndroid ? true : false, + page: () => DeferredWidget( + identity_creator.loadLibrary, + () => identity_creator.IdentityCreatorView() + ), + binding: IdentityCreatorBindings()), ] ]; diff --git a/lib/main/routes/app_routes.dart b/lib/main/routes/app_routes.dart index afc761e5a7..7fbf482c24 100644 --- a/lib/main/routes/app_routes.dart +++ b/lib/main/routes/app_routes.dart @@ -2,7 +2,7 @@ abstract class AppRoutes { static const home = '/'; static const login = '/login'; static const session = '/session'; - static const dashboard = '/dashboard/'; + static const dashboard = '/dashboard'; static const dashboardWithParameter = '/dashboard/:id'; static const settings = '/settings'; static const composer = '/composer'; @@ -14,4 +14,5 @@ abstract class AppRoutes { static const emailsForwardCreator = '/emails_forward_creator'; static const unknownRoutePage = '/notfound'; static const searchMailbox = '/search_mailbox'; + static const mailtoURL = '/mailto'; } \ No newline at end of file diff --git a/lib/main/routes/dialog_router.dart b/lib/main/routes/dialog_router.dart new file mode 100644 index 0000000000..786b50ae99 --- /dev/null +++ b/lib/main/routes/dialog_router.dart @@ -0,0 +1,62 @@ + +import 'package:core/utils/app_logger.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_bindings.dart'; +import 'package:tmail_ui_user/features/destination_picker/presentation/destination_picker_view.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_bindings.dart'; +import 'package:tmail_ui_user/features/identity_creator/presentation/identity_creator_view.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_bindings.dart'; +import 'package:tmail_ui_user/features/mailbox_creator/presentation/mailbox_creator_view.dart'; +import 'package:tmail_ui_user/features/rules_filter_creator/presentation/rules_filter_creator_bindings.dart'; +import 'package:tmail_ui_user/features/rules_filter_creator/presentation/rules_filter_creator_view.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; + +class DialogRouter { + + static Future pushGeneralDialog({ + required String routeName, + required Object? arguments + }) { + _bindingDI(routeName); + + return Get.generalDialog( + routeSettings: RouteSettings(arguments: arguments), + pageBuilder: (_, __, ___) => _generateView(routeName: routeName, arguments: arguments) + ); + } + + static void _bindingDI(String routeName) { + log('DialogRouter::_bindingDI():routeName: $routeName'); + switch(routeName) { + case AppRoutes.mailboxCreator: + MailboxCreatorBindings().dependencies(); + break; + case AppRoutes.rulesFilterCreator: + RulesFilterCreatorBindings().dependencies(); + break; + case AppRoutes.identityCreator: + IdentityCreatorBindings().dependencies(); + break; + case AppRoutes.destinationPicker: + DestinationPickerBindings().dependencies(); + break; + } + } + + static Widget _generateView({required String routeName, required Object? arguments}) { + log('DialogRouter::_generateView():routeName: $routeName | arguments: $arguments'); + switch(routeName) { + case AppRoutes.mailboxCreator: + return MailboxCreatorView(); + case AppRoutes.rulesFilterCreator: + return RuleFilterCreatorView(); + case AppRoutes.identityCreator: + return IdentityCreatorView(); + case AppRoutes.destinationPicker: + return DestinationPickerView(); + default: + return const SizedBox.shrink(); + } + } +} \ No newline at end of file diff --git a/lib/main/routes/navigation_router.dart b/lib/main/routes/navigation_router.dart index 368836fbb6..b87041721e 100644 --- a/lib/main/routes/navigation_router.dart +++ b/lib/main/routes/navigation_router.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; @@ -14,14 +15,30 @@ class NavigationRouter with EquatableMixin { final MailboxId? mailboxId; final DashboardType dashboardType; final SearchQuery? searchQuery; + final String? routeName; + final EmailAddress? emailAddress; + final String? subject; NavigationRouter({ this.emailId, this.mailboxId, this.searchQuery, - this.dashboardType = DashboardType.normal + this.dashboardType = DashboardType.normal, + this.routeName, + this.emailAddress, + this.subject, }); + factory NavigationRouter.initial() => NavigationRouter(); + @override - List get props => [emailId, mailboxId, searchQuery, dashboardType]; + List get props => [ + emailId, + mailboxId, + searchQuery, + dashboardType, + routeName, + emailAddress, + subject, + ]; } \ No newline at end of file diff --git a/lib/main/routes/route_navigation.dart b/lib/main/routes/route_navigation.dart index 2bfee5a652..005bd8fd5c 100644 --- a/lib/main/routes/route_navigation.dart +++ b/lib/main/routes/route_navigation.dart @@ -10,6 +10,10 @@ Future pushAndPop(String routeName, {dynamic arguments}) async { return Get.offNamed(routeName, arguments: arguments); } +Future popAndPush(String routeName, {dynamic arguments}) async { + return Get.offAndToNamed(routeName, arguments: arguments); +} + Future pushAndPopAll(String routeName, {dynamic arguments}) async { return Get.offAllNamed(routeName, arguments: arguments); } diff --git a/lib/main/routes/route_utils.dart b/lib/main/routes/route_utils.dart index 31afd4a7a5..9bf29a84cb 100644 --- a/lib/main/routes/route_utils.dart +++ b/lib/main/routes/route_utils.dart @@ -2,33 +2,48 @@ import 'package:core/data/model/query/query_parameter.dart'; import 'package:core/data/network/config/service_path.dart'; import 'package:core/utils/app_logger.dart'; -import 'package:core/utils/build_utils.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:get/get.dart'; import 'package:jmap_dart_client/jmap/core/id.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:tmail_ui_user/features/login/data/extensions/service_path_extension.dart'; import 'package:tmail_ui_user/features/thread/domain/model/search_query.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; import 'package:tmail_ui_user/main/routes/navigation_router.dart'; import 'package:universal_html/html.dart' as html; abstract class RouteUtils { + static const String paramID = 'id'; + static const String paramType = 'type'; + static const String paramContext = 'context'; + static const String paramQuery = 'q'; + static const String paramRouteName = 'routeName'; + static const String paramMailtoAddress = 'mailtoAddress'; + static const String paramSubject = 'subject'; + + static const String mailtoPrefix = 'mailto:'; + + static const String INVALID_VALUE = 'invalid'; + static String get baseOriginUrl => Uri.base.origin; static String get baseUrl => Uri.base.path; static String generateNavigationRoute(String route, NavigationRouter router) { ServicePath servicePath = ServicePath(route); - if (BuildUtils.isWeb) { + if (PlatformInfo.isWeb) { if (router.emailId != null) { servicePath = servicePath.withPathParameter(router.emailId!.id.value); } servicePath = servicePath.withQueryParameters([ - StringQueryParameter('type', router.dashboardType.name), + StringQueryParameter(paramType, router.dashboardType.name), if (router.mailboxId != null) - StringQueryParameter('context', router.mailboxId!.id.value), + StringQueryParameter(paramContext, router.mailboxId!.id.value), if (router.searchQuery != null) - StringQueryParameter('q', router.searchQuery!.value), + StringQueryParameter(paramQuery, router.searchQuery!.value), ]); return servicePath.path; } else { @@ -37,27 +52,30 @@ abstract class RouteUtils { } static Uri generateRouteBrowser(String route, NavigationRouter router) { - final baseRoutePath = '$baseOriginUrl/#$route'; + final baseRoutePath = '$baseOriginUrl$route'; ServicePath servicePath = ServicePath(baseRoutePath); if (router.emailId != null) { servicePath = servicePath.withPathParameter(router.emailId!.id.value); } servicePath = servicePath.withQueryParameters([ - StringQueryParameter('type', router.dashboardType.name), + StringQueryParameter(paramType, router.dashboardType.name), if (router.mailboxId != null) - StringQueryParameter('context', router.mailboxId!.id.value), + StringQueryParameter(paramContext, router.mailboxId!.id.value), if (router.searchQuery != null) - StringQueryParameter('q', router.searchQuery!.value), + StringQueryParameter(paramQuery, router.searchQuery!.value), ]); return Uri.parse(servicePath.path); } static NavigationRouter parsingRouteParametersToNavigationRouter(Map parameters) { - final idParam = parameters['id']; - final typeParam = parameters['type']; - final contextPram = parameters['context']; - final queryParam = parameters['q']; + final idParam = parameters[paramID]; + final typeParam = parameters[paramType]; + final contextPram = parameters[paramContext]; + final queryParam = parameters[paramQuery]; + final routeName = parameters[paramRouteName]; + final mailtoAddress = parameters[paramMailtoAddress]; + final subject = parameters[paramSubject]; final emailId = idParam != null ? EmailId(Id(idParam)) : null; final mailboxId = contextPram != null ? MailboxId(Id(contextPram)) : null; @@ -65,12 +83,18 @@ abstract class RouteUtils { final dashboardType = typeParam == DashboardType.search.name ? DashboardType.search : DashboardType.normal; + final emailAddress = mailtoAddress != null && GetUtils.isEmail(mailtoAddress) + ? EmailAddress(null, mailtoAddress) + : EmailAddress(null, INVALID_VALUE); return NavigationRouter( emailId: emailId, mailboxId: mailboxId, searchQuery: searchQuery, dashboardType: dashboardType, + routeName: routeName, + emailAddress: emailAddress, + subject: subject, ); } @@ -79,4 +103,31 @@ abstract class RouteUtils { log('RouteUtils::updateRouteOnBrowser(): newRoute: $newRoute'); html.window.history.replaceState(null, title, newRoute.toString()); } + + static Map parseMapMailtoFromUri(String? mailtoUri) { + log('RouteUtils::parseMapMailtoFromUri:mailtoUri: $mailtoUri'); + final mapMailto = { + RouteUtils.paramRouteName: AppRoutes.mailtoURL, + }; + if (mailtoUri?.startsWith(mailtoPrefix) == true) { + final mailtoUrlDecoded = Uri.decodeFull(mailtoUri!); + final uri = Uri.tryParse(mailtoUrlDecoded); + if (uri == null) return mapMailto; + + final mailtoAddress = uri.path; + final mapQueryParam = uri.queryParameters; + + mapMailto[paramMailtoAddress] = mailtoAddress; + if (mapQueryParam.containsKey(paramSubject)) { + mapMailto[paramSubject] = mapQueryParam[paramSubject]; + } + } else if (mailtoUri != null) { + final mailtoUrlDecoded = Uri.decodeFull(mailtoUri); + mapMailto[paramMailtoAddress] = mailtoUrlDecoded; + } else { + mapMailto[paramMailtoAddress] = mailtoUri; + } + log('RouteUtils::parseMapMailtoFromUri:mapMailto: $mapMailto'); + return mapMailto; + } } \ No newline at end of file diff --git a/lib/main/utils/app_config.dart b/lib/main/utils/app_config.dart index ab6620885a..4387167594 100644 --- a/lib/main/utils/app_config.dart +++ b/lib/main/utils/app_config.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; class AppConfig { static String get baseUrl => dotenv.get('SERVER_URL', fallback: ''); @@ -28,4 +29,18 @@ class AppConfig { static String appFCMConfigurationPath = "configurations/env.fcm"; static String get fcmVapidPublicKeyWeb => dotenv.get('FIREBASE_WEB_VAPID_PUBLIC_KEY', fallback: ''); + + static List get oidcScopes { + try { + final envScopes = dotenv.get('OIDC_SCOPES', fallback: ''); + + if (envScopes.isNotEmpty) { + return envScopes.split(','); + } + + return OIDCConstant.oidcScope; + } catch (e) { + return OIDCConstant.oidcScope; + } + } } \ No newline at end of file diff --git a/lib/main/utils/app_constants.dart b/lib/main/utils/app_constants.dart new file mode 100644 index 0000000000..c4b8fc78ab --- /dev/null +++ b/lib/main/utils/app_constants.dart @@ -0,0 +1,4 @@ + +class AppConstants { + static const int limitCharToStartSearch = 3; +} \ No newline at end of file diff --git a/lib/main/utils/app_utils.dart b/lib/main/utils/app_utils.dart index cfa6ebbb08..93a0723eda 100644 --- a/lib/main/utils/app_utils.dart +++ b/lib/main/utils/app_utils.dart @@ -1,7 +1,14 @@ -import 'package:core/core.dart'; +import 'package:core/utils/platform_info.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:get/get.dart'; +import 'package:tmail_ui_user/main/localizations/app_localizations.dart'; +import 'package:tmail_ui_user/main/localizations/language_code_constants.dart'; import 'package:tmail_ui_user/main/utils/app_config.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:intl/intl.dart' as intl; +import 'package:date_format/date_format.dart' as date_format; class AppUtils { @@ -22,8 +29,46 @@ class AppUtils { await launchUrl( Uri.parse(url), webOnlyWindowName: isNewTab ? '_blank' : '_self', + mode: LaunchMode.externalApplication ); } - static String? get fcmVapidPublicKey => BuildUtils.isWeb ? AppConfig.fcmVapidPublicKeyWeb : null; + static String? get fcmVapidPublicKey => PlatformInfo.isWeb ? AppConfig.fcmVapidPublicKeyWeb : null; + + static bool isDirectionRTL(BuildContext context) { + return intl.Bidi.isRtlLanguage(Localizations.localeOf(context).languageCode); + } + + static TextDirection getCurrentDirection(BuildContext context) => Directionality.maybeOf(context) ?? TextDirection.ltr; + + static bool isEmailLocalhost(String email) { + return RegExp(r'^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@localhost$').hasMatch(email); + } + + static void copyEmailAddressToClipboard(BuildContext context, String emailAddress) { + Clipboard.setData(ClipboardData(text: emailAddress)).then((_){ + ScaffoldMessenger.maybeOf(context)?.showSnackBar( + SnackBar(content: Text(AppLocalizations.of(context).email_address_copied_to_clipboard)) + ); + }); + } + + static date_format.DateLocale getCurrentDateLocale() { + final currentLanguageCode = Get.locale?.languageCode; + if (currentLanguageCode == LanguageCodeConstants.french) { + return const date_format.FrenchDateLocale(); + } else if (currentLanguageCode == LanguageCodeConstants.english) { + return const date_format.EnglishDateLocale(); + } else if (currentLanguageCode == LanguageCodeConstants.vietnamese) { + return const date_format.VietnameseDateLocale(); + } else if (currentLanguageCode == LanguageCodeConstants.russian) { + return const date_format.RussianDateLocale(); + } else if (currentLanguageCode == LanguageCodeConstants.arabic) { + return const date_format.ArabicDateLocale(); + } else if (currentLanguageCode == LanguageCodeConstants.italian) { + return const date_format.ItalianDateLocale(); + } else { + return const date_format.EnglishDateLocale(); + } + } } \ No newline at end of file diff --git a/lib/main/utils/email_receive_manager.dart b/lib/main/utils/email_receive_manager.dart index 86f32da3e8..4b0943c554 100644 --- a/lib/main/utils/email_receive_manager.dart +++ b/lib/main/utils/email_receive_manager.dart @@ -1,5 +1,6 @@ import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/email/email_content.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:rxdart/rxdart.dart'; @@ -8,6 +9,9 @@ class EmailReceiveManager { BehaviorSubject _pendingEmailAddressInfo = BehaviorSubject.seeded(null); BehaviorSubject get pendingEmailAddressInfo => _pendingEmailAddressInfo; + BehaviorSubject _pendingEmailContentInfo = BehaviorSubject.seeded(null); + BehaviorSubject get pendingEmailContentInfo => _pendingEmailContentInfo; + BehaviorSubject> _pendingFileInfo = BehaviorSubject.seeded(List.empty(growable: true)); BehaviorSubject> get pendingFileInfo => _pendingFileInfo; @@ -29,6 +33,19 @@ class EmailReceiveManager { _pendingEmailAddressInfo.add(emailAddress); } + void setPendingEmailContent(EmailContent emailContent) async { + clearPendingEmailAddress(); + _pendingEmailContentInfo.add(emailContent); + } + + void clearPendingEmailContent() { + if (_pendingEmailContentInfo.isClosed) { + _pendingEmailContentInfo = BehaviorSubject.seeded(null); + } else { + _pendingEmailContentInfo.add(null); + } + } + void clearPendingEmailAddress() { if(_pendingEmailAddressInfo.isClosed) { _pendingEmailAddressInfo = BehaviorSubject.seeded(null); diff --git a/model/analysis_options.yaml b/model/analysis_options.yaml index 61b6c4de17..0f32754d37 100644 --- a/model/analysis_options.yaml +++ b/model/analysis_options.yaml @@ -10,20 +10,7 @@ include: package:flutter_lints/flutter.yaml linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + constant_identifier_names: false + non_constant_identifier_names: false + unnecessary_string_escapes: false \ No newline at end of file diff --git a/model/lib/account/account_request.dart b/model/lib/account/account_request.dart index ba74490225..b285c0f05c 100644 --- a/model/lib/account/account_request.dart +++ b/model/lib/account/account_request.dart @@ -1,7 +1,10 @@ import 'dart:convert'; import 'package:equatable/equatable.dart'; -import 'package:model/model.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/authentication_type.dart'; +import 'package:model/account/password.dart'; +import 'package:model/oidc/token.dart'; class AccountRequest with EquatableMixin { final UserName? userName; @@ -16,8 +19,7 @@ class AccountRequest with EquatableMixin { this.authenticationType = AuthenticationType.none, }); - String get basicAuth => - 'Basic ${base64Encode(utf8.encode('${userName?.userName}:${password?.value}'))}'; + String get basicAuth => 'Basic ${base64Encode(utf8.encode('${userName?.value}:${password?.value}'))}'; String get bearerToken => 'Bearer ${token?.token}'; diff --git a/model/lib/account/authentication_type.dart b/model/lib/account/authentication_type.dart index c38842f07b..7ec25d7bc5 100644 --- a/model/lib/account/authentication_type.dart +++ b/model/lib/account/authentication_type.dart @@ -1,18 +1,5 @@ enum AuthenticationType { basic, oidc, - none -} - -extension AuthenticationTypeExtension on AuthenticationType { - String asString() { - switch (this) { - case AuthenticationType.oidc: - return 'oidc'; - case AuthenticationType.basic: - return 'basic'; - default: - return 'none'; - } - } + none; } \ No newline at end of file diff --git a/model/lib/account/jmap_account.dart b/model/lib/account/jmap_account.dart new file mode 100644 index 0000000000..3157e2dae6 --- /dev/null +++ b/model/lib/account/jmap_account.dart @@ -0,0 +1,32 @@ + +import 'package:equatable/equatable.dart'; +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/account/account.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart'; + +class JmapAccount with EquatableMixin { + + final AccountId accountId; + final AccountName name; + final bool isPersonal; + final bool isReadOnly; + final Map accountCapabilities; + + JmapAccount( + this.accountId, + this.name, + this.isPersonal, + this.isReadOnly, + this.accountCapabilities, + ); + + @override + List get props => [ + accountId, + name, + isPersonal, + isReadOnly, + accountCapabilities + ]; +} \ No newline at end of file diff --git a/model/lib/account/account.dart b/model/lib/account/personal_account.dart similarity index 73% rename from model/lib/account/account.dart rename to model/lib/account/personal_account.dart index f66b9c41c8..0042a33191 100644 --- a/model/lib/account/account.dart +++ b/model/lib/account/personal_account.dart @@ -1,21 +1,24 @@ import 'package:equatable/equatable.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; import 'package:model/account/authentication_type.dart'; -class Account with EquatableMixin { +class PersonalAccount with EquatableMixin { final String id; final AuthenticationType authenticationType; final bool isSelected; final AccountId? accountId; final String? apiUrl; + final UserName? userName; - Account( + PersonalAccount( this.id, this.authenticationType, { required this.isSelected, this.accountId, this.apiUrl, + this.userName } ); @@ -25,6 +28,7 @@ class Account with EquatableMixin { authenticationType, isSelected, accountId, - apiUrl + apiUrl, + userName ]; } \ No newline at end of file diff --git a/model/lib/account/user_name.dart b/model/lib/account/user_name.dart deleted file mode 100644 index 879f46e5e5..0000000000 --- a/model/lib/account/user_name.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class UserName with EquatableMixin { - final String userName; - - UserName(this.userName); - - @override - List get props => [userName]; -} \ No newline at end of file diff --git a/model/lib/contact/device_contact.dart b/model/lib/contact/device_contact.dart index a11ad9be12..a1cd4a7575 100644 --- a/model/lib/contact/device_contact.dart +++ b/model/lib/contact/device_contact.dart @@ -4,7 +4,4 @@ import 'package:model/contact/contact.dart'; class DeviceContact extends Contact implements EquatableMixin { DeviceContact(String displayName, String email) : super(displayName, email); - - @override - List get props => super.props; } diff --git a/model/lib/email/attachment.dart b/model/lib/email/attachment.dart index 444f8aaaad..a0f30e6b11 100644 --- a/model/lib/email/attachment.dart +++ b/model/lib/email/attachment.dart @@ -9,6 +9,9 @@ import 'package:uri/uri.dart'; class Attachment with EquatableMixin { + static const String eventICSSubtype = 'ics'; + static const String eventCalendarSubtype = 'calendar'; + final PartId? partId; final Id? blobId; final UnsignedInt? size; @@ -32,9 +35,9 @@ class Attachment with EquatableMixin { bool hasCid() => cid != null && cid?.isNotEmpty == true; String getDownloadUrl(String baseDownloadUrl, AccountId accountId) { - final downloadUriTemplate = UriTemplate('$baseDownloadUrl'); + final downloadUriTemplate = UriTemplate(baseDownloadUrl); final downloadUri = downloadUriTemplate.expand({ - 'accountId' : '${accountId.id.value}', + 'accountId' : accountId.id.value, 'blobId' : '${blobId?.value}', 'name' : '$name', 'type' : '${type?.mimeType}', @@ -68,7 +71,7 @@ extension ContentDispositionExtension on ContentDisposition { case ContentDisposition.attachment: return 'attachment'; case ContentDisposition.other: - return this.toString(); + return toString(); } } } diff --git a/model/lib/email/email_action_type.dart b/model/lib/email/email_action_type.dart index 161ee58fbf..2ea30b84ff 100644 --- a/model/lib/email/email_action_type.dart +++ b/model/lib/email/email_action_type.dart @@ -9,8 +9,13 @@ enum EmailActionType { markAsStarred, unMarkAsStarred, moveToMailbox, - edit, + editDraft, + editSendingEmail, + composeFromContentShared, + composeFromFileShared, composeFromEmailAddress, + composeFromMailtoUri, + reopenComposerBrowser, moveToTrash, deletePermanently, preview, diff --git a/model/lib/email/email_content.dart b/model/lib/email/email_content.dart index 7afd10a26e..be3108c83b 100644 --- a/model/lib/email/email_content.dart +++ b/model/lib/email/email_content.dart @@ -1,8 +1,5 @@ - -import 'package:core/utils/app_logger.dart'; import 'package:equatable/equatable.dart'; import 'package:model/email/email_content_type.dart'; -import 'package:model/model.dart'; class EmailContent with EquatableMixin { @@ -36,17 +33,14 @@ extension EmailContentExtension on EmailContent { return ''; } - final firstTags = ''; - final latestTags = ''; + const firstTags = ''; + const latestTags = ''; if (content.startsWith(firstTags) && content.endsWith(latestTags)) { - final firstIndex = firstTags.length; + const firstIndex = firstTags.length; final latestIndex = content.length - latestTags.length; - log('EmailContentExtension::_getContentOriginal(): firstIndex: $firstIndex'); - log('EmailContentExtension::_getContentOriginal(): latestIndex: $latestIndex'); if (latestIndex > firstIndex) { final contentOriginal = content.substring(firstIndex, latestIndex); - log('EmailContentExtension::_getContentOriginal(): contentOriginal: $contentOriginal'); return contentOriginal; } } diff --git a/model/lib/email/email_property.dart b/model/lib/email/email_property.dart index cfb7010f7f..f705555db8 100644 --- a/model/lib/email/email_property.dart +++ b/model/lib/email/email_property.dart @@ -18,5 +18,7 @@ class EmailProperty { static const String htmlBody = 'htmlBody'; static const String attachments = 'attachments'; static const String headers = 'headers'; - static final String headerMdnKey = 'Disposition-Notification-To'; + static const String headerMdnKey = 'Disposition-Notification-To'; + static const String messageId = 'messageId'; + static const String references = 'references'; } \ No newline at end of file diff --git a/model/lib/email/presentation_email.dart b/model/lib/email/presentation_email.dart index 532f6b214b..780c190876 100644 --- a/model/lib/email/presentation_email.dart +++ b/model/lib/email/presentation_email.dart @@ -5,15 +5,21 @@ import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; import 'package:jmap_dart_client/jmap/core/utc_date.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_body_value.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_header.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/email/email_content.dart'; import 'package:model/extensions/email_address_extension.dart'; +import 'package:model/extensions/media_type_nullable_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; import 'package:model/mailbox/select_mode.dart'; class PresentationEmail with EquatableMixin { - final EmailId id; + final EmailId? id; final Map? keywords; final UnsignedInt? size; final UTCDate? receivedAt; @@ -27,35 +33,45 @@ class PresentationEmail with EquatableMixin { final Set? bcc; final Set? replyTo; final Map? mailboxIds; - final List? mailboxNames; final SelectMode selectMode; final Uri? routeWeb; final PresentationMailbox? mailboxContain; + final List? emailHeader; + final Set? htmlBody; + final Map? bodyValues; + final Map? headerCalendarEvent; - PresentationEmail( + PresentationEmail({ this.id, - { - this.keywords, - this.size, - this.receivedAt, - this.hasAttachment, - this.preview, - this.subject, - this.sentAt, - this.from, - this.to, - this.cc, - this.bcc, - this.replyTo, - this.mailboxIds, - this.mailboxNames, - this.selectMode = SelectMode.INACTIVE, - this.routeWeb, - this.mailboxContain - } - ); + this.keywords, + this.size, + this.receivedAt, + this.hasAttachment, + this.preview, + this.subject, + this.sentAt, + this.from, + this.to, + this.cc, + this.bcc, + this.replyTo, + this.mailboxIds, + this.selectMode = SelectMode.INACTIVE, + this.routeWeb, + this.mailboxContain, + this.emailHeader, + this.htmlBody, + this.bodyValues, + this.headerCalendarEvent, + }); - String getSenderName() => from?.first.asString() ?? ''; + String getSenderName() { + if (from?.isNotEmpty == true) { + return from?.first.asString() ?? ''; + } else { + return ''; + } + } String getAvatarText() { if (getSenderName().isNotEmpty) { @@ -74,31 +90,56 @@ class PresentationEmail with EquatableMixin { bool get isDraft => keywords?.containsKey(KeyWordIdentifier.emailDraft) == true; - bool get withAttachments => hasAttachment == true; + bool get isAnswered => keywords?.containsKey(KeyWordIdentifier.emailAnswered) == true; - String get mailboxName => mailboxNames?.first?.name ?? ''; + bool get isForwarded => keywords?.containsKey(KeyWordIdentifier.emailForwarded) == true; + + bool get isAnsweredAndForwarded => isAnswered && isForwarded; + + bool get withAttachments => hasAttachment == true; String get routeWebAsString => routeWeb.toString(); + bool get pushNotificationActivated => !isDraft && !hasRead; + + bool get hasCalendarEvent => headerCalendarEvent?[IndividualHeaderIdentifier.headerCalendarEvent]?.isNotEmpty == true; + + List get emailContentList { + final newHtmlBody = htmlBody + ?.where((emailBody) => emailBody.partId != null && emailBody.type != null) + .toList() ?? []; + + final mapHtmlBody = { for (var emailBody in newHtmlBody) emailBody.partId! : emailBody.type! }; + + final emailContents = bodyValues?.entries + .map((entries) => EmailContent(mapHtmlBody[entries.key].toEmailContentType(), entries.value.value)) + .toList(); + + return emailContents ?? []; + } + @override List get props => [ id, + keywords, + size, + receivedAt, + hasAttachment, + preview, subject, + sentAt, from, to, cc, bcc, - keywords, - size, - receivedAt, - sentAt, replyTo, - preview, - hasAttachment, mailboxIds, - mailboxNames, selectMode, routeWeb, - mailboxContain + mailboxContain, + emailHeader, + htmlBody, + bodyValues, + headerCalendarEvent, ]; } \ No newline at end of file diff --git a/model/lib/error_type_handler/account_exception.dart b/model/lib/error_type_handler/account_exception.dart new file mode 100644 index 0000000000..a2c6a25676 --- /dev/null +++ b/model/lib/error_type_handler/account_exception.dart @@ -0,0 +1,2 @@ + +class NotFoundPersonalAccountException implements Exception {} \ No newline at end of file diff --git a/model/lib/extensions/account_extension.dart b/model/lib/extensions/account_extension.dart index b841773469..6fb84b04a5 100644 --- a/model/lib/extensions/account_extension.dart +++ b/model/lib/extensions/account_extension.dart @@ -1,16 +1,15 @@ import 'package:jmap_dart_client/jmap/account_id.dart'; -import 'package:model/account/account.dart'; +import 'package:jmap_dart_client/jmap/core/account/account.dart'; +import 'package:model/account/jmap_account.dart'; extension AccountExtension on Account { - - Account fromAccountId({required AccountId accountId, required String apiUrl}) { - return Account( - id, - authenticationType, - isSelected: isSelected, - accountId: accountId, - apiUrl: apiUrl - ); + JmapAccount toJmapAccount(AccountId accountId) { + return JmapAccount( + accountId, + name, + isPersonal, + isReadOnly, + accountCapabilities); } } \ No newline at end of file diff --git a/model/lib/extensions/account_id_extensions.dart b/model/lib/extensions/account_id_extensions.dart new file mode 100644 index 0000000000..6337f45904 --- /dev/null +++ b/model/lib/extensions/account_id_extensions.dart @@ -0,0 +1,6 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; + +extension AccountIdExtension on AccountId { + String get asString => id.value; +} \ No newline at end of file diff --git a/model/lib/extensions/email_extension.dart b/model/lib/extensions/email_extension.dart index f525f561a3..afee2f592f 100644 --- a/model/lib/extensions/email_extension.dart +++ b/model/lib/extensions/email_extension.dart @@ -1,19 +1,26 @@ +import 'dart:convert'; + import 'package:jmap_dart_client/jmap/core/properties/properties.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart'; +import 'package:jmap_dart_client/jmap/mail/email/individual_header_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/email/keyword_identifier.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; extension EmailExtension on Email { + String asString() => jsonEncode(toJson()); + bool get hasRead => keywords?.containsKey(KeyWordIdentifier.emailSeen) == true; bool get hasStarred => keywords?.containsKey(KeyWordIdentifier.emailFlagged) == true; bool get hasMdnSent => keywords?.containsKey(KeyWordIdentifier.mdnSent) == true; + bool get isDraft => keywords?.containsKey(KeyWordIdentifier.emailDraft) == true; + bool get withAttachments => hasAttachment == true; bool hasReadReceipt(Map mapMailbox) { @@ -34,30 +41,31 @@ extension EmailExtension on Email { Email updatedEmail({Map? newKeywords, Map? newMailboxIds}) { return Email( - id, - keywords: newKeywords ?? keywords, - size: size, - receivedAt: receivedAt, - hasAttachment: hasAttachment, - preview: preview, - subject: subject, - sentAt: sentAt, - from: from, - to: to, - cc: cc, - bcc: bcc, - replyTo: replyTo, - mailboxIds: newMailboxIds ?? mailboxIds, - htmlBody: htmlBody, - bodyValues: bodyValues, - headerUserAgent: headerUserAgent, - attachments: attachments + id: id, + keywords: newKeywords ?? keywords, + size: size, + receivedAt: receivedAt, + hasAttachment: hasAttachment, + preview: preview, + subject: subject, + sentAt: sentAt, + from: from, + to: to, + cc: cc, + bcc: bcc, + replyTo: replyTo, + mailboxIds: newMailboxIds ?? mailboxIds, + htmlBody: htmlBody, + bodyValues: bodyValues, + headerUserAgent: headerUserAgent, + attachments: attachments, + headerCalendarEvent: headerCalendarEvent ); } PresentationEmail toPresentationEmail({SelectMode selectMode = SelectMode.INACTIVE}) { return PresentationEmail( - id, + id: id, keywords: keywords, size: size, receivedAt: receivedAt, @@ -72,12 +80,14 @@ extension EmailExtension on Email { replyTo: replyTo, mailboxIds: mailboxIds, selectMode: selectMode, + emailHeader: headers?.toList(), + headerCalendarEvent: headerCalendarEvent ); } Email combineEmail(Email newEmail, Properties updatedProperties) { return Email( - newEmail.id, + id: newEmail.id, keywords: updatedProperties.contain(EmailProperty.keywords) ? newEmail.keywords : keywords, size: updatedProperties.contain(EmailProperty.size) ? newEmail.size : size, receivedAt: updatedProperties.contain(EmailProperty.receivedAt) ? newEmail.receivedAt : receivedAt, @@ -91,6 +101,7 @@ extension EmailExtension on Email { bcc: updatedProperties.contain(EmailProperty.bcc) ? newEmail.bcc : bcc, replyTo: updatedProperties.contain(EmailProperty.replyTo) ? newEmail.replyTo : replyTo, mailboxIds: updatedProperties.contain(EmailProperty.mailboxIds) ? newEmail.mailboxIds : mailboxIds, + headerCalendarEvent: updatedProperties.contain(IndividualHeaderIdentifier.headerCalendarEvent.value) ? newEmail.headerCalendarEvent : headerCalendarEvent, ); } @@ -108,15 +119,9 @@ extension EmailExtension on Email { return emailContents ?? []; } - List get allAttachments { - if (attachments != null) { - return attachments! - .where((element) => element.disposition != null) - .map((item) => item.toAttachment()) - .toList(); - } - return []; - } + List get allAttachments => attachments?.map((item) => item.toAttachment()).toList() ?? []; + + List get attachmentsWithCid => allAttachments.where((attachment) => attachment.hasCid()).toList(); PresentationMailbox? findMailboxContain(Map mapMailbox) { final newMailboxIds = mailboxIds; @@ -130,4 +135,33 @@ extension EmailExtension on Email { } return null; } + + PresentationEmail sendingEmailToPresentationEmail( + { + SelectMode selectMode = SelectMode.INACTIVE, + EmailId? emailId, + } + ) { + return PresentationEmail( + id: emailId ?? id, + keywords: keywords, + size: size, + receivedAt: receivedAt, + hasAttachment: hasAttachment, + preview: preview, + subject: subject, + sentAt: sentAt, + from: from, + to: to, + cc: cc, + bcc: bcc, + replyTo: replyTo, + mailboxIds: mailboxIds, + selectMode: selectMode, + emailHeader: headers?.toList(), + bodyValues: bodyValues, + htmlBody: htmlBody, + headerCalendarEvent: headerCalendarEvent + ); + } } \ No newline at end of file diff --git a/model/lib/extensions/email_id_extensions.dart b/model/lib/extensions/email_id_extensions.dart new file mode 100644 index 0000000000..2841f331ac --- /dev/null +++ b/model/lib/extensions/email_id_extensions.dart @@ -0,0 +1,5 @@ +import 'package:jmap_dart_client/jmap/mail/email/email.dart'; + +extension EmailIdExtension on EmailId { + String get asString => id.value; +} \ No newline at end of file diff --git a/model/lib/extensions/identity_id_extension.dart b/model/lib/extensions/identity_id_extension.dart new file mode 100644 index 0000000000..11f2885736 --- /dev/null +++ b/model/lib/extensions/identity_id_extension.dart @@ -0,0 +1,6 @@ + +import 'package:jmap_dart_client/jmap/identities/identity.dart'; + +extension IdentityIdExtension on IdentityId { + String get asString => id.value; +} \ No newline at end of file diff --git a/model/lib/extensions/keyword_identifier_extension.dart b/model/lib/extensions/keyword_identifier_extension.dart index 909b126402..84fb93e947 100644 --- a/model/lib/extensions/keyword_identifier_extension.dart +++ b/model/lib/extensions/keyword_identifier_extension.dart @@ -19,4 +19,16 @@ extension KeyWordIdentifierExtension on KeyWordIdentifier { generatePath(): markStarAction == MarkStarAction.markStar ? true : null }); } + + PatchObject generateAnsweredActionPath() { + return PatchObject({ + generatePath(): true + }); + } + + PatchObject generateForwardedActionPath() { + return PatchObject({ + generatePath(): true + }); + } } \ No newline at end of file diff --git a/model/lib/extensions/list_attachment_extension.dart b/model/lib/extensions/list_attachment_extension.dart index dd8f7ed729..fa9c02d7a2 100644 --- a/model/lib/extensions/list_attachment_extension.dart +++ b/model/lib/extensions/list_attachment_extension.dart @@ -1,4 +1,6 @@ -import 'package:model/model.dart'; + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:model/email/attachment.dart'; extension ListAttachmentExtension on List { @@ -11,13 +13,18 @@ extension ListAttachmentExtension on List { return 0; } - List get listAttachmentsDisplayedOutSide { - return where((attachment) => attachment.disposition == ContentDisposition.attachment || attachment.noCid()) - .toList(); - } + List get listAttachmentsDisplayedOutSide => where((attachment) => attachment.noCid()).toList(); + + List get listAttachmentsDisplayedInContent => where((attachment) => attachment.hasCid()).toList(); - List get listAttachmentsDisplayedInContent { - return where((attachment) => attachment.hasCid()) - .toList(); + Map toMapCidImageDownloadUrl({ + required AccountId accountId, + required String downloadUrl + }) { + final mapUrlDownloadCID = { + for (var attachment in listAttachmentsDisplayedInContent) + attachment.cid! : attachment.getDownloadUrl(downloadUrl, accountId) + }; + return mapUrlDownloadCID; } } \ No newline at end of file diff --git a/model/lib/extensions/list_email_address_extension.dart b/model/lib/extensions/list_email_address_extension.dart index 22e1a2828c..41be64533b 100644 --- a/model/lib/extensions/list_email_address_extension.dart +++ b/model/lib/extensions/list_email_address_extension.dart @@ -8,6 +8,8 @@ extension ListEmailAddressExtension on Set? { List asList() => this != null ? this!.toList() : List.empty(); + Set asSet() => this ?? {}; + List emailAddressToListString({ExpandMode expandMode = ExpandMode.EXPAND, int limitAddress = 1, bool isFullEmailAddress = false}) { if (this != null) { if (expandMode == ExpandMode.EXPAND) { @@ -27,10 +29,9 @@ extension ListEmailAddressExtension on Set? { int numberEmailAddress() => this != null ? this!.length : 0; - List filterEmailAddress(EmailAddress emailAddressNotExist) { + List filterEmailAddress(String emailAddressNotExist) { return this != null - ? this!.where((emailAddress) => emailAddress.email != emailAddressNotExist.email) - .toList() + ? this!.where((emailAddress) => emailAddress.email != emailAddressNotExist).toList() : List.empty(); } } \ No newline at end of file diff --git a/model/lib/extensions/list_email_extension.dart b/model/lib/extensions/list_email_extension.dart index f0bd55d5e2..a4a609e48c 100644 --- a/model/lib/extensions/list_email_extension.dart +++ b/model/lib/extensions/list_email_extension.dart @@ -1,4 +1,5 @@ +import 'package:collection/collection.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/core/sort/comparator.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator_property.dart'; @@ -8,6 +9,8 @@ import 'package:jmap_dart_client/jmap/core/extensions/unsigned_int_extension.dar extension ListEmailExtension on List { + List get listEmailIds => map((email) => email.id).whereNotNull().toList(); + Email? findEmailById(EmailId emailId) { try { return firstWhere((email) => email.id == emailId); diff --git a/model/lib/extensions/list_email_id_extension.dart b/model/lib/extensions/list_email_id_extension.dart index e5f344a176..82fbebdcd5 100644 --- a/model/lib/extensions/list_email_id_extension.dart +++ b/model/lib/extensions/list_email_id_extension.dart @@ -10,34 +10,44 @@ extension ListEmailIdExtension on List { List toIds() => map((emailId) => emailId.id).toList(); Map generateMapUpdateObjectMarkAsRead(ReadActions readActions) { - final Map maps = {}; - forEach((emailId) { - maps[emailId.id] = KeyWordIdentifier.emailSeen.generateReadActionPath(readActions); - }); - return maps; + return { + for (var emailId in this) + emailId.id: KeyWordIdentifier.emailSeen.generateReadActionPath(readActions) + }; } Map generateMapUpdateObjectMoveToMailbox(MailboxId currentMailboxId, MailboxId destinationMailboxId) { - final Map maps = {}; - forEach((emailId) { - maps[emailId.id] = currentMailboxId.generateMoveToMailboxActionPath(destinationMailboxId); - }); - return maps; + return { + for (var emailId in this) + emailId.id: currentMailboxId.generateMoveToMailboxActionPath(destinationMailboxId) + }; } Map generateMapUpdateObjectMarkAsStar(MarkStarAction markStarAction) { - final Map maps = {}; - forEach((emailId) { - maps[emailId.id] = KeyWordIdentifier.emailFlagged.generateMarkStarActionPath(markStarAction); - }); - return maps; + return { + for (var emailId in this) + emailId.id: KeyWordIdentifier.emailFlagged.generateMarkStarActionPath(markStarAction) + }; } Map generateMapUpdateObjectMarkAsSpam(MailboxId spamMailboxId) { - final Map maps = {}; - forEach((emailId) { - maps[emailId.id] = spamMailboxId.generateActionPath(); - }); - return maps; + return { + for (var emailId in this) + emailId.id: spamMailboxId.generateActionPath() + }; + } + + Map generateMapUpdateObjectMarkAsAnswered() { + return { + for (var emailId in this) + emailId.id: KeyWordIdentifier.emailAnswered.generateAnsweredActionPath() + }; + } + + Map generateMapUpdateObjectMarkAsForwarded() { + return { + for (var emailId in this) + emailId.id: KeyWordIdentifier.emailForwarded.generateForwardedActionPath() + }; } } \ No newline at end of file diff --git a/model/lib/extensions/list_identity_id_extension.dart b/model/lib/extensions/list_identity_id_extension.dart new file mode 100644 index 0000000000..6ce94bf874 --- /dev/null +++ b/model/lib/extensions/list_identity_id_extension.dart @@ -0,0 +1,16 @@ +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/patch_object.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/identities/identity.dart'; +import 'package:model/identity/identity_request_dto.dart'; + +extension ListIdentityIdExtension on List { + + Map generateMapUpdateObjectSortOrder({UnsignedInt? sortOrder}) { + final Map maps = {}; + forEach((identityId) { + maps[identityId.id] = PatchObject(IdentityRequestDto(sortOrder: sortOrder).toJson()); + }); + return maps; + } +} \ No newline at end of file diff --git a/model/lib/extensions/list_presentation_email_extension.dart b/model/lib/extensions/list_presentation_email_extension.dart index 4cac4d68bc..b5351c7c73 100644 --- a/model/lib/extensions/list_presentation_email_extension.dart +++ b/model/lib/extensions/list_presentation_email_extension.dart @@ -1,4 +1,6 @@ +import 'package:collection/collection.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; import 'package:model/model.dart'; @@ -18,7 +20,7 @@ extension ListPresentationEmailExtension on List { return where((email) => email.selectMode == SelectMode.ACTIVE).toList(); } - List get listEmailIds => map((email) => email.id).toList(); + List get listEmailIds => map((email) => email.id).whereNotNull().toList(); bool isAllCanDeletePermanently(Map mapMailbox) { final listMailboxContain = map((email) => email.findMailboxContain(mapMailbox)) @@ -88,9 +90,13 @@ extension ListPresentationEmailExtension on List { List combine(List listEmailBefore) { return map((presentationEmail) { - final emailBefore = listEmailBefore.findEmail(presentationEmail.id); - if (emailBefore != null) { - return presentationEmail.toSelectedEmail(selectMode: emailBefore.selectMode); + if (presentationEmail.id != null) { + final emailBefore = listEmailBefore.findEmail(presentationEmail.id!); + if (emailBefore != null) { + return presentationEmail.toSelectedEmail(selectMode: emailBefore.selectMode); + } else { + return presentationEmail; + } } else { return presentationEmail; } @@ -98,4 +104,13 @@ extension ListPresentationEmailExtension on List { } int matchedIndex(EmailId emailId) => indexWhere((email) => email.id == emailId); + + List toEmailsAvailablePushNotification({List? mailboxIdsNotPutNotifications}) { + log('ListPresentationEmailExtension::toEmailsAvailablePushNotification():mailboxIdsNotPutNotifications: $mailboxIdsNotPutNotifications'); + if (mailboxIdsNotPutNotifications?.isNotEmpty == true) { + return where((email) => !email.isBelongToOneOfTheMailboxes(mailboxIdsNotPutNotifications!) && email.pushNotificationActivated).toList(); + } else { + return where((email) => email.pushNotificationActivated).toList(); + } + } } \ No newline at end of file diff --git a/model/lib/extensions/list_presentation_mailbox_extension.dart b/model/lib/extensions/list_presentation_mailbox_extension.dart index 330c56c056..1ca05f5b98 100644 --- a/model/lib/extensions/list_presentation_mailbox_extension.dart +++ b/model/lib/extensions/list_presentation_mailbox_extension.dart @@ -1,11 +1,21 @@ +import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; +import 'package:model/extensions/presentation_mailbox_extension.dart'; import 'package:model/mailbox/presentation_mailbox.dart'; extension ListPresentationMailboxExtension on List { - List get listSubscribedMailboxes => - where((mailbox) => mailbox.supportedSubscribe).toList(); + List get listSubscribedMailboxesAndDefaultMailboxes => + where((mailbox) => mailbox.isSubscribedMailbox || mailbox.isDefault).toList(); List get listPersonalMailboxes => where((mailbox) => mailbox.isPersonal).toList(); + + bool get isAllPersonalMailboxes => every((mailbox) => mailbox.isPersonal && !mailbox.isDefault); + + bool get isAllDefaultMailboxes => every((mailbox) => mailbox.isDefault); + + bool get isAllUnreadMailboxes => every((mailbox) => mailbox.countUnReadEmailsAsString.isNotEmpty); + + List get mailboxIds => map((mailbox) => mailbox.id).toList(); } \ No newline at end of file diff --git a/model/lib/extensions/mailbox_id_extension.dart b/model/lib/extensions/mailbox_id_extension.dart index 89fee49136..b6e45b1529 100644 --- a/model/lib/extensions/mailbox_id_extension.dart +++ b/model/lib/extensions/mailbox_id_extension.dart @@ -23,4 +23,6 @@ extension MailboxIdExtension on MailboxId { generatePath(): true, }); } + + String get asString => id.value; } \ No newline at end of file diff --git a/model/lib/extensions/media_type_extension.dart b/model/lib/extensions/media_type_nullable_extension.dart similarity index 89% rename from model/lib/extensions/media_type_extension.dart rename to model/lib/extensions/media_type_nullable_extension.dart index 6a86e46391..826cacecd1 100644 --- a/model/lib/extensions/media_type_extension.dart +++ b/model/lib/extensions/media_type_nullable_extension.dart @@ -1,7 +1,7 @@ import 'package:http_parser/http_parser.dart'; import 'package:model/email/email_content_type.dart'; -extension MediaTypeExtension on MediaType? { +extension MediaTypeNullableExtension on MediaType? { EmailContentType toEmailContentType() { if (this == null) { diff --git a/model/lib/extensions/personal_account_extension.dart b/model/lib/extensions/personal_account_extension.dart new file mode 100644 index 0000000000..aaada3c3b0 --- /dev/null +++ b/model/lib/extensions/personal_account_extension.dart @@ -0,0 +1,21 @@ + +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; +import 'package:model/account/personal_account.dart'; + +extension PersonalAccountExtension on PersonalAccount { + + PersonalAccount fromAccount({ + required AccountId accountId, + required String apiUrl, + required UserName userName, + }) { + return PersonalAccount( + id, + authenticationType, + isSelected: isSelected, + accountId: accountId, + apiUrl: apiUrl, + userName: userName); + } +} \ No newline at end of file diff --git a/model/lib/extensions/presentation_email_extension.dart b/model/lib/extensions/presentation_email_extension.dart index 857626eed7..a14b367331 100644 --- a/model/lib/extensions/presentation_email_extension.dart +++ b/model/lib/extensions/presentation_email_extension.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:core/domain/extensions/datetime_extension.dart'; import 'package:core/presentation/extensions/color_extension.dart'; +import 'package:core/utils/app_logger.dart'; import 'package:jmap_dart_client/jmap/mail/email/email.dart'; import 'package:dartz/dartz.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; @@ -33,9 +34,11 @@ extension PresentationEmailExtension on PresentationEmail { return ''; } + Set get listEmailAddressSender => from.asSet()..addAll(replyTo.asSet()); + PresentationEmail toggleSelect() { return PresentationEmail( - this.id, + id: this.id, keywords: keywords, size: size, receivedAt: receivedAt, @@ -49,16 +52,16 @@ extension PresentationEmailExtension on PresentationEmail { bcc: bcc, replyTo: replyTo, mailboxIds: mailboxIds, - mailboxNames: mailboxNames, selectMode: selectMode == SelectMode.INACTIVE ? SelectMode.ACTIVE : SelectMode.INACTIVE, routeWeb: routeWeb, - mailboxContain: mailboxContain + mailboxContain: mailboxContain, + headerCalendarEvent: headerCalendarEvent ); } PresentationEmail toSelectedEmail({required SelectMode selectMode}) { return PresentationEmail( - this.id, + id: this.id, keywords: keywords, size: size, receivedAt: receivedAt, @@ -72,28 +75,33 @@ extension PresentationEmailExtension on PresentationEmail { bcc: bcc, replyTo: replyTo, mailboxIds: mailboxIds, - mailboxNames: mailboxNames, selectMode: selectMode, routeWeb: routeWeb, - mailboxContain: mailboxContain + mailboxContain: mailboxContain, + headerCalendarEvent: headerCalendarEvent ); } Email toEmail() { return Email( - this.id, - keywords: keywords, - size: size, - receivedAt: receivedAt, - hasAttachment: hasAttachment, - preview: preview, - subject: subject, - sentAt: sentAt, - from: from, - to: to, - cc: cc, - bcc: bcc, - replyTo: replyTo + id: this.id, + keywords: keywords, + size: size, + receivedAt: receivedAt, + hasAttachment: hasAttachment, + preview: preview, + subject: subject, + sentAt: sentAt, + from: from, + to: to, + cc: cc, + bcc: bcc, + replyTo: replyTo, + htmlBody: htmlBody, + bodyValues: bodyValues, + mailboxIds: mailboxIds, + headers: emailHeader?.toSet(), + headerCalendarEvent: headerCalendarEvent ); } @@ -102,16 +110,16 @@ extension PresentationEmailExtension on PresentationEmail { return allEmailAddress.isNotEmpty ? allEmailAddress.join(', ') : ''; } - Tuple3, List, List> generateRecipientsEmailAddressForComposer( - EmailActionType? emailActionType, - Role? mailboxRole - ) { + Tuple3, List, List> generateRecipientsEmailAddressForComposer({ + required EmailActionType emailActionType, + Role? mailboxRole + }) { switch(emailActionType) { case EmailActionType.reply: if (mailboxRole == PresentationMailbox.roleSent) { return Tuple3(to.asList(), [], []); } else { - final replyToAddress = replyTo.asList().isNotEmpty ? replyTo.asList() : from.asList(); + final replyToAddress = replyTo.asList().isNotEmpty ? to.asList() + replyTo.asList() : from.asList(); return Tuple3(replyToAddress, [], []); } case EmailActionType.replyAll: @@ -120,28 +128,18 @@ extension PresentationEmailExtension on PresentationEmail { } else { return Tuple3(to.asList() + from.asList(), cc.asList(), bcc.asList()); } - case EmailActionType.edit: - return Tuple3(to.asList(), cc.asList(), bcc.asList()); default: - return const Tuple3([], [], []); + return Tuple3(to.asList(), cc.asList(), bcc.asList()); } } PresentationEmail toSearchPresentationEmail(Map mapMailboxes) { mailboxIds?.removeWhere((key, value) => !value); - final listMailboxId = mailboxIds?.entries - .where((entry) => entry.value) - .map((entry) => entry.key) - .toList(); - - final listMailboxName = listMailboxId - ?.map((mailboxId) => mapMailboxes.containsKey(mailboxId) ? mapMailboxes[mailboxId]?.name : null) - .where((mailboxName) => mailboxName != null) - .toList(); + final matchedMailbox = findMailboxContain(mapMailboxes); return PresentationEmail( - this.id, + id: this.id, keywords: keywords, size: size, receivedAt: receivedAt, @@ -155,10 +153,10 @@ extension PresentationEmailExtension on PresentationEmail { bcc: bcc, replyTo: replyTo, mailboxIds: mailboxIds, - mailboxNames: listMailboxName, selectMode: selectMode, routeWeb: routeWeb, - mailboxContain: mailboxContain + mailboxContain: matchedMailbox, + headerCalendarEvent: headerCalendarEvent ); } @@ -177,7 +175,7 @@ extension PresentationEmailExtension on PresentationEmail { PresentationEmail withRouteWeb(Uri routeWeb) { return PresentationEmail( - this.id, + id: this.id, keywords: keywords, size: size, receivedAt: receivedAt, @@ -191,16 +189,16 @@ extension PresentationEmailExtension on PresentationEmail { bcc: bcc, replyTo: replyTo, mailboxIds: mailboxIds, - mailboxNames: mailboxNames, selectMode: selectMode, routeWeb: routeWeb, - mailboxContain: mailboxContain + mailboxContain: mailboxContain, + headerCalendarEvent: headerCalendarEvent ); } PresentationEmail updateKeywords(Map? newKeywords) { return PresentationEmail( - this.id, + id: this.id, keywords: newKeywords, size: size, receivedAt: receivedAt, @@ -214,16 +212,16 @@ extension PresentationEmailExtension on PresentationEmail { bcc: bcc, replyTo: replyTo, mailboxIds: mailboxIds, - mailboxNames: mailboxNames, selectMode: selectMode, routeWeb: routeWeb, - mailboxContain: mailboxContain + mailboxContain: mailboxContain, + headerCalendarEvent: headerCalendarEvent ); } PresentationEmail syncPresentationEmail({PresentationMailbox? mailboxContain, Uri? routeWeb}) { return PresentationEmail( - this.id, + id: this.id, keywords: keywords, size: size, receivedAt: receivedAt, @@ -237,10 +235,25 @@ extension PresentationEmailExtension on PresentationEmail { bcc: bcc, replyTo: replyTo, mailboxIds: mailboxIds, - mailboxNames: mailboxNames, selectMode: selectMode, routeWeb: routeWeb, - mailboxContain: mailboxContain + mailboxContain: mailboxContain, + headerCalendarEvent: headerCalendarEvent ); } + + bool isBelongToOneOfTheMailboxes(List mailboxIdsSource) { + final mapMailboxIds = mailboxIds; + mapMailboxIds?.removeWhere((key, value) => !value); + + if (mapMailboxIds?.isNotEmpty == true) { + final listMailboxId = mapMailboxIds!.keys.toList(); + log('PresentationEmailExtension::isBelongToOneOfTheMailboxes():listMailboxId: $listMailboxId'); + final listMailboxIdValid = listMailboxId.where((mailboxId) => mailboxIdsSource.contains(mailboxId)); + log('PresentationEmailExtension::isBelongToOneOfTheMailboxes():listMailboxIdValid: $listMailboxIdValid'); + return listMailboxIdValid.isNotEmpty; + } + + return false; + } } \ No newline at end of file diff --git a/model/lib/extensions/presentation_mailbox_extension.dart b/model/lib/extensions/presentation_mailbox_extension.dart index 4b2e6cbb11..9efa5d7140 100644 --- a/model/lib/extensions/presentation_mailbox_extension.dart +++ b/model/lib/extensions/presentation_mailbox_extension.dart @@ -1,8 +1,83 @@ import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart'; -import 'package:model/model.dart'; +import 'package:jmap_dart_client/jmap/mail/mailbox/namespace.dart'; +import 'package:model/mailbox/mailbox_state.dart'; +import 'package:model/mailbox/presentation_mailbox.dart'; +import 'package:model/mailbox/select_mode.dart'; extension PresentationMailboxExtension on PresentationMailbox { + bool get isActivated => state == MailboxState.activated; + + bool hasParentId() => parentId != null && parentId!.id.value.isNotEmpty; + + bool hasRole() => role != null && role!.value.isNotEmpty; + + bool get isDefault => hasRole(); + + bool get isPersonal => namespace == null || namespace == Namespace('Personal'); + + bool get isTeamMailboxes => !isPersonal && !hasParentId(); + + bool get isChildOfTeamMailboxes => !isPersonal && hasParentId(); + + String get countUnReadEmailsAsString { + if (countUnreadEmails <= 0) return ''; + return countUnreadEmails <= 999 ? '$countUnreadEmails' : '999+'; + } + + int get countUnreadEmails => unreadEmails?.value.value.toInt() ?? 0; + + int get countTotalEmails => totalEmails?.value.value.toInt() ?? 0; + + bool get isInbox => role == PresentationMailbox.roleInbox; + + bool get isSpam => role == PresentationMailbox.roleSpam; + + bool get isTrash => role == PresentationMailbox.roleTrash; + + bool get isDrafts => role == PresentationMailbox.roleDrafts; + + bool get isTemplates => role == PresentationMailbox.roleTemplates; + + bool get isSent => role == PresentationMailbox.roleSent; + + bool get isOutbox => name == PresentationMailbox.lowerCaseOutboxMailboxName || role == PresentationMailbox.roleOutbox; + + bool get isSubscribedMailbox => isSubscribed != null && isSubscribed?.value == true; + + bool get allowedToDisplayCountOfUnreadEmails => !(isTrash || isSpam || isDrafts || isTemplates || isSent) && countUnreadEmails > 0; + + bool get allowedToDisplayCountOfTotalEmails => (isTrash || isSpam) && countTotalEmails > 0; + + bool get allowedHasEmptyAction => (isTrash || isSpam) && countTotalEmails > 0; + + String get countTotalEmailsAsString { + if (countTotalEmails <= 0) return ''; + return countTotalEmails <= 999 ? '$countTotalEmails' : '999+'; + } + + String get emailTeamMailBoxes { + final name = namespace?.value ?? ''; + if (name.isNotEmpty == true && + name.indexOf('[') > 0 && + name.indexOf(']') > name.indexOf('[')) { + return name.substring(name.indexOf('[') + 1, name.indexOf(']')); + } + return name; + } + + bool get allowedToDisplay => isSubscribedMailbox || isDefault; + + MailboxId? get mailboxId { + if (id == PresentationMailbox.unifiedMailbox.id) { + return null; + } else { + return id; + } + } + + bool get pushNotificationDeactivated => isOutbox || isSent || isDrafts || isTrash || isSpam; + PresentationMailbox toPresentationMailboxWithMailboxPath(String mailboxPath) { return PresentationMailbox( id, @@ -20,26 +95,49 @@ extension PresentationMailboxExtension on PresentationMailbox { mailboxPath: mailboxPath, state: state, namespace: namespace, + displayName: displayName, + ); + } + + PresentationMailbox withDisplayName(String? displayName) { + return PresentationMailbox( + id, + name: name, + parentId: parentId, + role: role, + sortOrder: sortOrder, + totalEmails: totalEmails, + unreadEmails: unreadEmails, + totalThreads: totalThreads, + unreadThreads: unreadThreads, + myRights: myRights, + isSubscribed: isSubscribed, + selectMode: selectMode, + mailboxPath: mailboxPath, + state: state, + namespace: namespace, + displayName: displayName, ); } PresentationMailbox withMailboxSate(MailboxState newMailboxState) { return PresentationMailbox( - id, - name: name, - parentId: parentId, - role: role, - sortOrder: sortOrder, - totalEmails: totalEmails, - unreadEmails: unreadEmails, - totalThreads: totalThreads, - unreadThreads: unreadThreads, - myRights: myRights, - isSubscribed: isSubscribed, - selectMode: selectMode, - mailboxPath: mailboxPath, - state: newMailboxState, - namespace: namespace, + id, + name: name, + parentId: parentId, + role: role, + sortOrder: sortOrder, + totalEmails: totalEmails, + unreadEmails: unreadEmails, + totalThreads: totalThreads, + unreadThreads: unreadThreads, + myRights: myRights, + isSubscribed: isSubscribed, + selectMode: selectMode, + mailboxPath: mailboxPath, + state: newMailboxState, + namespace: namespace, + displayName: displayName, ); } @@ -62,21 +160,22 @@ extension PresentationMailboxExtension on PresentationMailbox { PresentationMailbox toggleSelectPresentationMailbox() { return PresentationMailbox( - id, - name: name, - parentId: parentId, - role: role, - sortOrder: sortOrder, - totalEmails: totalEmails, - unreadEmails: unreadEmails, - totalThreads: totalThreads, - unreadThreads: unreadThreads, - myRights: myRights, - isSubscribed: isSubscribed, - mailboxPath: mailboxPath, - selectMode: selectMode == SelectMode.INACTIVE ? SelectMode.ACTIVE : SelectMode.INACTIVE, - state: state, - namespace: namespace, + id, + name: name, + parentId: parentId, + role: role, + sortOrder: sortOrder, + totalEmails: totalEmails, + unreadEmails: unreadEmails, + totalThreads: totalThreads, + unreadThreads: unreadThreads, + myRights: myRights, + isSubscribed: isSubscribed, + mailboxPath: mailboxPath, + selectMode: selectMode == SelectMode.INACTIVE ? SelectMode.ACTIVE : SelectMode.INACTIVE, + state: state, + namespace: namespace, + displayName: displayName, ); } @@ -97,6 +196,7 @@ extension PresentationMailboxExtension on PresentationMailbox { selectMode: selectMode, state: state, namespace: namespace, + displayName: displayName, ); } } \ No newline at end of file diff --git a/model/lib/extensions/session_extension.dart b/model/lib/extensions/session_extension.dart index 9621a17707..5d9da0a65f 100644 --- a/model/lib/extensions/session_extension.dart +++ b/model/lib/extensions/session_extension.dart @@ -1,31 +1,37 @@ -import 'package:core/utils/app_logger.dart'; +import 'package:core/presentation/extensions/uri_extension.dart'; import 'package:jmap_dart_client/jmap/account_id.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; import 'package:jmap_dart_client/jmap/core/capability/capability_properties.dart'; import 'package:jmap_dart_client/jmap/core/capability/empty_capability.dart'; import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:model/error_type_handler/account_exception.dart'; +import 'package:model/model.dart'; import 'package:uri/uri.dart'; extension SessionExtension on Session { - String getDownloadUrl() { - var baseUrl = '${downloadUrl.origin}${downloadUrl.path}?${downloadUrl.query}'; + + String getDownloadUrl({String? jmapUrl}) { + final downloadUrlValid = jmapUrl != null + ? downloadUrl.toQualifiedUrl(baseUrl: Uri.parse(jmapUrl)) + : downloadUrl; + var baseUrl = '${downloadUrlValid.origin}${downloadUrlValid.path}?${downloadUrlValid.query}'; if (baseUrl.endsWith('/')) { baseUrl = baseUrl.substring(0, baseUrl.length - 1); } - log('SessionExtension::getDownloadUrl(): $baseUrl'); final downloadUrlDecode = Uri.decodeFull(baseUrl); - log('SessionExtension::getDownloadUrl(): DECODE $downloadUrlDecode'); return downloadUrlDecode; } - Uri getUploadUri(AccountId accountId) { - final baseUrl = '${uploadUrl.origin}${uploadUrl.path}'; - final uploadUriTemplate = UriTemplate('${Uri.decodeFull(baseUrl)}'); + Uri getUploadUri(AccountId accountId, {String? jmapUrl}) { + final uploadUrlValid = jmapUrl != null + ? uploadUrl.toQualifiedUrl(baseUrl: Uri.parse(jmapUrl)) + : uploadUrl; + final baseUrl = '${uploadUrlValid.origin}${uploadUrlValid.path}'; + final uploadUriTemplate = UriTemplate(Uri.decodeFull(baseUrl)); final uploadUri = uploadUriTemplate.expand({ - 'accountId' : '${accountId.id.value}' + 'accountId' : accountId.id.value }); - log('SessionExtension::getUploadUri(): uploadUri: $uploadUri'); return Uri.parse(uploadUri); } @@ -39,4 +45,18 @@ extension SessionExtension on Session { } return (capability as T); } + + JmapAccount get personalAccount { + if (accounts.isNotEmpty) { + final listPersonalAccount = accounts.entries + .map((entry) => entry.value.toJmapAccount(entry.key)) + .where((jmapAccount) => jmapAccount.isPersonal) + .toList(); + + if (listPersonalAccount.isNotEmpty) { + return listPersonalAccount.first; + } + } + throw NotFoundPersonalAccountException(); + } } \ No newline at end of file diff --git a/model/lib/mailbox/presentation_mailbox.dart b/model/lib/mailbox/presentation_mailbox.dart index e8882a3493..90ab329b1c 100644 --- a/model/lib/mailbox/presentation_mailbox.dart +++ b/model/lib/mailbox/presentation_mailbox.dart @@ -8,15 +8,24 @@ import 'package:model/mailbox/select_mode.dart'; class PresentationMailbox with EquatableMixin { + static const String inboxRole = 'inbox'; + static const String sentRole = 'sent'; + static const String trashRole = 'trash'; + static const String templatesRole= 'templates'; + static const String outboxRole = 'outbox'; + static const String draftsRole = 'drafts'; + static const String spamRole = 'spam'; + static const String archiveRole = 'archive'; + static final PresentationMailbox unifiedMailbox = PresentationMailbox(MailboxId(Id('unified'))); - static final roleInbox = Role('inbox'); - static final roleTrash = Role('trash'); - static final roleSent = Role('sent'); - static final roleTemplates = Role('templates'); - static final roleOutbox = Role('outbox'); - static final roleDrafts = Role('drafts'); - static final roleSpam = Role('spam'); + static final roleInbox = Role(inboxRole); + static final roleTrash = Role(trashRole); + static final roleSent = Role(sentRole); + static final roleTemplates = Role(templatesRole); + static final roleOutbox = Role(outboxRole); + static final roleDrafts = Role(draftsRole); + static final roleSpam = Role(spamRole); static final outboxMailboxName = MailboxName('Outbox'); static final lowerCaseOutboxMailboxName = MailboxName('outbox'); @@ -36,6 +45,7 @@ class PresentationMailbox with EquatableMixin { final String? mailboxPath; final MailboxState? state; final Namespace? namespace; + final String? displayName; PresentationMailbox( this.id, @@ -54,79 +64,10 @@ class PresentationMailbox with EquatableMixin { this.mailboxPath, this.state = MailboxState.activated, this.namespace, + this.displayName, } ); - bool get isActivated => state == MailboxState.activated; - - bool hasParentId() => parentId != null && parentId!.id.value.isNotEmpty; - - bool hasRole() => role != null && role!.value.isNotEmpty; - - bool get isPersonal => namespace == null || namespace == Namespace('Personal'); - - bool get isTeamMailboxes => !isPersonal && !hasParentId(); - - bool get isChildOfTeamMailboxes => !isPersonal && hasParentId(); - - String getCountUnReadEmails() { - if (unreadEmails == null || unreadEmails!.value.value <= 0) { - return ''; - } - - return unreadEmails!.value.value <= 999 ? '${unreadEmails!.value.value}' : '999+'; - } - - bool get isSpam => role == roleSpam; - - bool get isTrash => role == roleTrash; - - bool get isDrafts => role == roleDrafts; - - bool get isTemplates => role == roleTemplates; - - bool get isSent => role == roleSent; - - bool get isOutbox => name == lowerCaseOutboxMailboxName || role == roleOutbox; - - bool get isSubscribedMailbox => isSubscribed != null && isSubscribed?.value == true; - - bool matchCountingRules() { - if (isTrash || isDrafts || isTemplates || isSent) { - return false; - } else { - return true; - } - } - - String? get emailTeamMailBoxes => namespace?.value.substring( - (namespace?.value.indexOf('[') ?? 0) + 1, - namespace?.value.indexOf(']')); - - bool get supportedSubscribe => isSubscribed?.value == true; - - bool get isSupportedDisableMailbox { - if (!supportedSubscribe) { - return false; - } - if (isPersonal) { - return true; - } else { - return isTeamMailboxes; - } - } - - bool get isSupportedEnableMailbox { - if (supportedSubscribe) { - return false; - } - if (isPersonal) { - return true; - } else { - return isTeamMailboxes; - } - } - @override List get props => [ id, @@ -144,5 +85,6 @@ class PresentationMailbox with EquatableMixin { mailboxPath, state, namespace, + displayName, ]; } \ No newline at end of file diff --git a/model/lib/model.dart b/model/lib/model.dart index 2114416011..40e7c379dd 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -1,12 +1,12 @@ library model; -export 'account/account.dart'; +// Account +export 'account/personal_account.dart'; export 'account/account_request.dart'; export 'account/account_type.dart'; export 'account/authentication_type.dart'; export 'account/password.dart'; -// Account -export 'account/user_name.dart'; +export 'account/jmap_account.dart'; // AutoComplete export 'autocomplete/auto_complete_pattern.dart'; // Contact @@ -43,7 +43,7 @@ export 'extensions/list_presentation_email_extension.dart'; export 'extensions/mailbox_extension.dart'; export 'extensions/mailbox_id_extension.dart'; export 'extensions/mailbox_name_extension.dart'; -export 'extensions/media_type_extension.dart'; +export 'extensions/media_type_nullable_extension.dart'; export 'extensions/presentation_email_extension.dart'; export 'extensions/presentation_mailbox_extension.dart'; export 'extensions/properties_extension.dart'; @@ -52,8 +52,13 @@ export 'extensions/user_profile_extension.dart'; export 'extensions/utc_date_extension.dart'; export 'extensions/email_filter_condition_extension.dart'; export 'extensions/list_email_header_extension.dart'; -export 'extensions/account_extension.dart'; +export 'extensions/personal_account_extension.dart'; export 'extensions/list_presentation_mailbox_extension.dart'; +export 'extensions/list_identity_id_extension.dart'; +export 'extensions/email_id_extensions.dart'; +export 'extensions/account_id_extensions.dart'; +export 'extensions/account_extension.dart'; +export 'extensions/identity_id_extension.dart'; // Identity export 'identity/identity_request_dto.dart'; export 'mailbox/expand_mode.dart'; @@ -66,6 +71,7 @@ export 'oidc/oidc_configuration.dart'; export 'oidc/request/oidc_request.dart'; export 'oidc/response/oidc_link_dto.dart'; // OIDC +export 'oidc/response/oidc_discovery_response.dart'; export 'oidc/response/oidc_response.dart'; export 'oidc/token.dart'; export 'oidc/token_id.dart'; diff --git a/model/lib/oidc/oidc_configuration.dart b/model/lib/oidc/oidc_configuration.dart index 5d992c7983..ebec65a3dc 100644 --- a/model/lib/oidc/oidc_configuration.dart +++ b/model/lib/oidc/oidc_configuration.dart @@ -21,7 +21,7 @@ class OIDCConfiguration with EquatableMixin { if (authority.endsWith('/')) { return authority + wellKnownOpenId; } else { - return authority + '/' + wellKnownOpenId; + return '$authority/$wellKnownOpenId'; } } diff --git a/model/lib/oidc/response/oidc_discovery_response.dart b/model/lib/oidc/response/oidc_discovery_response.dart new file mode 100644 index 0000000000..9553f3a053 --- /dev/null +++ b/model/lib/oidc/response/oidc_discovery_response.dart @@ -0,0 +1,21 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'oidc_discovery_response.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class OIDCDiscoveryResponse with EquatableMixin { + + final String? authorizationEndpoint; + final String? tokenEndpoint; + final String? endSessionEndpoint; + + OIDCDiscoveryResponse(this.authorizationEndpoint, this.tokenEndpoint, this.endSessionEndpoint); + + factory OIDCDiscoveryResponse.fromJson(Map json) => _$OIDCDiscoveryResponseFromJson(json); + + Map toJson() => _$OIDCDiscoveryResponseToJson(this); + + @override + List get props => [authorizationEndpoint, tokenEndpoint, endSessionEndpoint]; +} \ No newline at end of file diff --git a/model/lib/upload/file_info.dart b/model/lib/upload/file_info.dart index 7612093cf4..60580b06b9 100644 --- a/model/lib/upload/file_info.dart +++ b/model/lib/upload/file_info.dart @@ -1,6 +1,4 @@ -import 'dart:typed_data'; - import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:mime/mime.dart'; @@ -18,9 +16,17 @@ class FileInfo with EquatableMixin { return FileInfo('', '', 0); } + factory FileInfo.fromStream({ + required Stream> stream, + String? name, + int? size + }) { + return FileInfo(name ?? '', '', size ?? 0, readStream: stream); + } + String get fileExtension => fileName.split('.').last; - String get mimeType => lookupMimeType(kIsWeb ? fileName : filePath) ?? 'application/json; charset=UTF-8'; + String get mimeType => lookupMimeType(kIsWeb ? fileName : filePath) ?? 'application/octet-stream'; @override List get props => [fileName, filePath, fileSize, readStream, bytes]; diff --git a/model/pubspec.lock b/model/pubspec.lock index 5f78cf9c2c..0a83ff0e40 100644 --- a/model/pubspec.lock +++ b/model/pubspec.lock @@ -5,149 +5,162 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: a36ec4843dc30ea6bf652bf25e3448db6c5e8bcf4aa55f063a5d1dad216d8214 + url: "https://pub.dev" source: hosted - version: "34.0.0" + version: "58.0.0" analyzer: - dependency: "direct dev" + dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: cc4242565347e98424ce9945c819c192ec0838cb9d1f6aa4a97cc96becbc5b27 + url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "5.10.0" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.2.0" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" source: hosted version: "7.2.7" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" + url: "https://pub.dev" source: hosted - version: "8.4.2" + version: "8.4.4" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" source: hosted - version: "2.0.1" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.5" + version: "2.0.2" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.4.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection - url: "https://pub.dartlang.org" + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.1" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" core: dependency: "direct main" description: @@ -159,128 +172,122 @@ packages: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352" + url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" dartz: - dependency: transitive + dependency: "direct main" description: name: dartz - url: "https://pub.dartlang.org" + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" source: hosted version: "0.10.1" device_info_plus: dependency: transitive description: name: device_info_plus - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.2" - device_info_plus_linux: - dependency: transitive - description: - name: device_info_plus_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.1" - device_info_plus_macos: - dependency: transitive - description: - name: device_info_plus_macos - url: "https://pub.dartlang.org" + sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" + url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "8.1.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.6.1" - device_info_plus_web: - dependency: transitive - description: - name: device_info_plus_web - url: "https://pub.dartlang.org" + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" source: hosted - version: "2.1.0" - device_info_plus_windows: - dependency: transitive - description: - name: device_info_plus_windows - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.3" + version: "7.0.0" dio: dependency: transitive description: name: dio - url: "https://pub.dartlang.org" + sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.0.0" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted version: "6.1.4" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" flex_color_picker: dependency: transitive description: name: flex_color_picker - url: "https://pub.dartlang.org" + sha256: f0e0db8e3e47435cfbe9aa15c71b898fa218be0fc4ae409e1e42d5d5266b2c90 + url: "https://pub.dev" + source: hosted + version: "3.2.0" + flex_seed_scheme: + dependency: transitive + description: + name: flex_seed_scheme + sha256: "7058288ef97d348657ac95cea25d65a9aac181ca08387ede891fd7230ad7600f" + url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "1.2.3" flutter: dependency: "direct main" description: flutter @@ -290,137 +297,194 @@ packages: dependency: transitive description: name: flutter_image_compress - url: "https://pub.dartlang.org" + sha256: "37f1b26399098e5f97b74c1483f534855e7dff68ead6ddaccf747029fb03f29f" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview: + dependency: transitive + description: + name: flutter_inappwebview + sha256: "6d6c741ddba1dba5229d63ba75767064791a7ce845196b45e31105e93d67c949" + url: "https://pub.dev" + source: hosted + version: "6.0.0-beta.22" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "064a8ccbc76217dcd3b0fd6c6ea6f549e69b2849a0233b5bb46af9632c3ce2ff" + url: "https://pub.dev" source: hosted version: "1.1.0" flutter_keyboard_visibility: dependency: transitive description: name: flutter_keyboard_visibility - url: "https://pub.dartlang.org" + sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" + url: "https://pub.dev" + source: hosted + version: "5.4.1" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_keyboard_visibility_platform_interface: dependency: transitive description: name: flutter_keyboard_visibility_platform_interface - url: "https://pub.dartlang.org" + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" source: hosted version: "2.0.0" flutter_keyboard_visibility_web: dependency: transitive description: name: flutter_keyboard_visibility_web - url: "https://pub.dartlang.org" + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" source: hosted version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_svg: dependency: transitive description: name: flutter_svg - url: "https://pub.dartlang.org" + sha256: "97c5b291b4fd34ae4f55d6a4c05841d4d0ed94952e033c5a6529e1b47b4d2a29" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "2.0.2" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_typeahead: + dependency: transitive + description: + name: flutter_typeahead + sha256: f31211a8536f87908c3dcbdb88666e2f4d77f5f06c2b3a48eaad5599969ff32d + url: "https://pub.dev" + source: hosted + version: "4.6.0" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" - fluttertoast: - dependency: transitive - description: - name: fluttertoast - url: "https://pub.dartlang.org" - source: hosted - version: "8.0.8" frontend_server_client: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.2.0" get: dependency: transitive description: name: get - url: "https://pub.dartlang.org" + sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + url: "https://pub.dev" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" source: hosted version: "2.2.0" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.15.1" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted - version: "0.13.4" + version: "0.13.5" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted version: "3.2.1" http_parser: dependency: "direct main" description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" intl: dependency: "direct main" description: name: intl - url: "https://pub.dartlang.org" + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.0" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" jmap_dart_client: dependency: "direct main" description: path: "." ref: master - resolved-ref: "1df4203f3c7cb5f24ee68f3e1060efb7283dd3fd" + resolved-ref: e8005e28b48ee06259d4f51045a58f20c891e0b9 url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" @@ -428,154 +492,240 @@ packages: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.7" json_annotation: dependency: "direct main" description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.8.0" json_serializable: dependency: "direct dev" description: name: json_serializable - url: "https://pub.dartlang.org" + sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a + url: "https://pub.dev" + source: hosted + version: "6.6.1" + linkify: + dependency: transitive + description: + name: linkify + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "5.0.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.15" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.9.1" mime: dependency: "direct main" description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.1" - path_drawing: + version: "1.8.3" + path_parsing: dependency: transitive description: - name: path_drawing - url: "https://pub.dartlang.org" + name: path_parsing + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" source: hosted version: "1.0.1" - path_parsing: + path_provider: dependency: transitive description: - name: path_parsing - url: "https://pub.dartlang.org" + name: path_provider + sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.13" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + url: "https://pub.dev" + source: hosted + version: "2.0.27" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + url: "https://pub.dev" + source: hosted + version: "2.1.11" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: c2af5a8a6369992d915f8933dfc23172071001359d17896e83db8be57db8a397 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + url: "https://pub.dev" + source: hosted + version: "2.1.7" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.0" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pointer_interceptor: dependency: transitive description: name: pointer_interceptor - url: "https://pub.dartlang.org" + sha256: "49e6b86ba931d801ce852990d4a8913726ea3964266559e0b058baa3b4408435" + url: "https://pub.dev" source: hosted version: "0.9.1" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" quiver: dependency: "direct main" description: name: quiver - url: "https://pub.dartlang.org" + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" source: hosted - version: "3.0.1+1" + version: "3.2.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted version: "1.0.3" sky_engine: @@ -587,247 +737,258 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298 + url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.7" source_helper: dependency: transitive description: name: source_helper - url: "https://pub.dartlang.org" + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" - sqflite: - dependency: transitive - description: - name: sqflite - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "2.0.0+4" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - url: "https://pub.dartlang.org" - source: hosted - version: "2.4.0" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - synchronized: - dependency: transitive - description: - name: synchronized - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "3.0.0+3" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.5.1" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" universal_html: dependency: transitive description: name: universal_html - url: "https://pub.dartlang.org" + sha256: b5061c64c7c863c12e46279e032976f1c274f927fb3589b52b5928dcd2d52f7c + url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.0.9" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.dartlang.org" + sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.0" uri: dependency: "direct main" description: name: uri - url: "https://pub.dartlang.org" + sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" + url: "https://pub.dev" source: hosted version: "1.0.0" url_launcher: dependency: transitive description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" + url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.1.10" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: dd729390aa936bf1bdf5cd1bc7468ff340263f80a2c4f569416507667de8e3c8 + url: "https://pub.dev" source: hosted - version: "6.0.21" + version: "6.0.26" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: "3dedc66ca3c0bef9e6a93c0999aee102556a450afcc1b7bcfeace7a424927d92" + url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.1.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.4" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.4" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.16" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd + url: "https://pub.dev" source: hosted - version: "3.0.1" - vector_math: + version: "3.0.5" + vector_graphics: dependency: transitive description: - name: vector_math - url: "https://pub.dartlang.org" + name: vector_graphics + sha256: "4cf8e60dbe4d3a693d37dff11255a172594c0793da542183cbfe7fe978ae4aaa" + url: "https://pub.dev" source: hosted - version: "2.1.2" - watcher: + version: "1.1.4" + vector_graphics_codec: dependency: transitive description: - name: watcher - url: "https://pub.dartlang.org" + name: vector_graphics_codec + sha256: "278ad5f816f58b1967396d1f78ced470e3e58c9fe4b27010102c0a595c764468" + url: "https://pub.dev" source: hosted - version: "1.0.2" - web_socket_channel: + version: "1.1.4" + vector_graphics_compiler: dependency: transitive description: - name: web_socket_channel - url: "https://pub.dartlang.org" + name: vector_graphics_compiler + sha256: "0bf61ad56e6fd6688a2865d3ceaea396bc6a0a90ea0d7ad5049b1b76c09d6163" + url: "https://pub.dev" source: hosted - version: "2.2.0" - webview_flutter: + version: "1.1.4" + vector_math: dependency: transitive description: - name: webview_flutter - url: "https://pub.dartlang.org" + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "3.0.0" - webview_flutter_android: + version: "2.1.4" + watcher: dependency: transitive description: - name: webview_flutter_android - url: "https://pub.dartlang.org" + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted - version: "2.10.4" - webview_flutter_platform_interface: + version: "1.0.2" + web_socket_channel: dependency: transitive description: - name: webview_flutter_platform_interface - url: "https://pub.dartlang.org" + name: web_socket_channel + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" source: hosted - version: "1.9.5" - webview_flutter_wkwebview: + version: "2.3.0" + win32: dependency: transitive description: - name: webview_flutter_wkwebview - url: "https://pub.dartlang.org" + name: win32 + sha256: "5cdbe09a75b5f4517adf213c68aaf53ffa162fadf54ba16f663f94f3d2664a56" + url: "https://pub.dev" source: hosted - version: "2.9.5" - win32: + version: "4.1.1" + xdg_directories: dependency: transitive description: - name: win32 - url: "https://pub.dartlang.org" + name: xdg_directories + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "1.0.0" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.2" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.7.0" diff --git a/model/pubspec.yaml b/model/pubspec.yaml index 0beb0c7296..579d0d3aa0 100644 --- a/model/pubspec.yaml +++ b/model/pubspec.yaml @@ -20,7 +20,7 @@ publish_to: none version: 1.0.0+1 environment: - sdk: ">=2.16.2 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: @@ -29,48 +29,42 @@ dependencies: core: path: ../core - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 - - # equatable - equatable: 2.0.3 - - # quiver - quiver: 3.0.1+1 - - # intl - intl: 0.17.0 - - # jmap_dart_client + ### Dependencies from git ### jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git ref: master - # uri + ### Dependencies from pub.dev ### + cupertino_icons: 1.0.5 + + equatable: 2.0.5 + + quiver: 3.2.1 + + intl: 0.18.0 + uri: 1.0.0 - # json_annotation - json_annotation: 4.5.0 + json_annotation: 4.8.0 + + http_parser: 4.0.2 - # http_parser - http_parser: 4.0.0 + mime: 1.0.4 - # mime - mime: 1.0.1 + collection: 1.17.1 + + dartz: 0.10.1 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: 1.0.4 - - build_runner: 2.1.11 + flutter_lints: 2.0.1 - json_serializable: 6.2.0 + build_runner: 2.3.3 - analyzer: 3.2.0 + json_serializable: 6.6.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/prebuild.sh b/prebuild.sh index ee26aa5192..9ad7105a21 100644 --- a/prebuild.sh +++ b/prebuild.sh @@ -8,26 +8,26 @@ cd core flutter pub get ## Install necessary pods -#cd ../ios -#flutter pub get && pod install +# cd ../ios +# flutter pub get && pod install cd ../model -flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs +flutter pub get && dart run build_runner build --delete-conflicting-outputs cd ../contact -flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs +flutter pub get && dart run build_runner build --delete-conflicting-outputs cd ../forward -flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs +flutter pub get && dart run build_runner build --delete-conflicting-outputs cd ../rule_filter -flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs +flutter pub get && dart run build_runner build --delete-conflicting-outputs cd ../fcm -flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs +flutter pub get && dart run build_runner build --delete-conflicting-outputs cd .. flutter pub get \ - && flutter pub run build_runner build --delete-conflicting-outputs \ - && flutter pub run intl_generator:extract_to_arb --output-dir=./lib/l10n lib/main/localizations/app_localizations.dart \ - && flutter pub run intl_generator:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/main/localizations/app_localizations.dart lib/l10n/intl*.arb + && dart run build_runner build --delete-conflicting-outputs \ + && dart run intl_generator:extract_to_arb --output-dir=./lib/l10n lib/main/localizations/app_localizations.dart \ + && dart run intl_generator:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/main/localizations/app_localizations.dart lib/l10n/intl*.arb diff --git a/pubspec.lock b/pubspec.lock index 2f195ae706..c0009c4681 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,219 +5,210 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "61.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - url: "https://pub.dartlang.org" + sha256: "64fcb0dbca4386356386c085142fa6e79c00a3326ceaa778a2d25f5d9ba61441" + url: "https://pub.dev" source: hosted - version: "1.0.11" + version: "1.0.16" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "5.13.0" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d + url: "https://pub.dev" source: hosted - version: "3.3.5" + version: "3.3.6" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" async: dependency: "direct main" description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" better_open_file: dependency: "direct main" description: name: better_open_file - url: "https://pub.dartlang.org" + sha256: aa67d9bc38160401e48023833f93c789b4e2b1c0eb8b6643bd5816d0c1c65dbd + url: "https://pub.dev" source: hosted - version: "3.6.3" + version: "3.6.4" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.2.0" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" source: hosted version: "7.2.7" built_collection: dependency: "direct main" description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" + url: "https://pub.dev" source: hosted - version: "8.4.3" + version: "8.4.4" byte_converter: dependency: "direct main" description: name: byte_converter - url: "https://pub.dartlang.org" + sha256: "1ae161c9a3ff63eecf43e29316c7a27becfc0b77f2c64fd09de58ded5ac38d53" + url: "https://pub.dev" source: hosted version: "1.3.0" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" source: hosted - version: "2.0.1" - clock: - dependency: transitive - description: - name: clock - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - cloud_firestore_platform_interface: + version: "2.0.2" + cli_util: dependency: transitive description: - name: cloud_firestore_platform_interface - url: "https://pub.dartlang.org" + name: cli_util + sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + url: "https://pub.dev" source: hosted - version: "5.10.0" - cloud_firestore_web: + version: "0.3.5" + clock: dependency: transitive description: - name: cloud_firestore_web - url: "https://pub.dartlang.org" + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.5.0" collection: dependency: "direct main" description: name: collection - url: "https://pub.dartlang.org" + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.1" connectivity_plus: dependency: "direct main" description: name: connectivity_plus - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.1" - connectivity_plus_linux: - dependency: transitive - description: - name: connectivity_plus_linux - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - connectivity_plus_macos: - dependency: transitive - description: - name: connectivity_plus_macos - url: "https://pub.dartlang.org" + sha256: "8875e8ed511a49f030e313656154e4bbbcef18d68dfd32eb853fac10bce48e96" + url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "3.0.3" connectivity_plus_platform_interface: dependency: transitive description: name: connectivity_plus_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.3" - connectivity_plus_web: - dependency: transitive - description: - name: connectivity_plus_web - url: "https://pub.dartlang.org" + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.dev" source: hosted - version: "1.2.5" - connectivity_plus_windows: - dependency: transitive - description: - name: connectivity_plus_windows - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.2" + version: "1.2.4" contact: dependency: "direct main" description: @@ -238,9 +229,10 @@ packages: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" core: dependency: "direct main" description: @@ -248,145 +240,148 @@ packages: relative: true source: path version: "1.0.0+1" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + url: "https://pub.dev" + source: hosted + version: "0.3.3+4" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 + url: "https://pub.dev" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://pub.dartlang.org" + sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + url: "https://pub.dev" source: hosted version: "1.0.5" - cupertino_list_tile: - dependency: transitive - description: - name: cupertino_list_tile - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.1" cupertino_progress_bar: dependency: transitive description: name: cupertino_progress_bar - url: "https://pub.dartlang.org" + sha256: "4962a7c6db9b94d1c19462ad6036fdf20db43defa4114c908a3c4cbef1b3a2e0" + url: "https://pub.dev" source: hosted version: "0.2.0" cupertino_stepper: dependency: transitive description: name: cupertino_stepper - url: "https://pub.dartlang.org" + sha256: "124690b8b23db7b43fc6d547688e42a9f3d73c8f46d0b210568e98a04b5645b2" + url: "https://pub.dev" source: hosted version: "0.2.1" custom_pop_up_menu: dependency: "direct main" description: name: custom_pop_up_menu - url: "https://pub.dartlang.org" + sha256: eeac484c6ddffffb25e803dc2a5cc9381e700a29f074e9fcc76fe36b62fde850 + url: "https://pub.dev" source: hosted version: "1.2.4" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352" + url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" dartz: dependency: "direct main" description: name: dartz - url: "https://pub.dartlang.org" + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" source: hosted version: "0.10.1" + date_format: + dependency: "direct main" + description: + name: date_format + sha256: "8e5154ca363411847220c8cbc43afcf69c08e8debe40ba09d57710c25711760c" + url: "https://pub.dev" + source: hosted + version: "2.0.7" dbus: dependency: transitive description: name: dbus - url: "https://pub.dartlang.org" + sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + url: "https://pub.dev" source: hosted version: "0.7.8" debounce_throttle: - dependency: transitive + dependency: "direct main" description: name: debounce_throttle - url: "https://pub.dartlang.org" + sha256: c95cf47afda975fc507794a52040a16756fb2f31ad3027d4e691c41862ff5692 + url: "https://pub.dev" source: hosted version: "2.0.0" device_info_plus: dependency: "direct main" description: name: device_info_plus - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.2" - device_info_plus_linux: - dependency: transitive - description: - name: device_info_plus_linux - url: "https://pub.dartlang.org" + sha256: "1d6e5a61674ba3a68fb048a7c7b4ff4bebfed8d7379dbe8f2b718231be9a7c95" + url: "https://pub.dev" source: hosted - version: "2.1.1" - device_info_plus_macos: - dependency: transitive - description: - name: device_info_plus_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.3" + version: "8.1.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" source: hosted - version: "2.6.1" - device_info_plus_web: - dependency: transitive - description: - name: device_info_plus_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - device_info_plus_windows: - dependency: transitive - description: - name: device_info_plus_windows - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.3" + version: "7.0.0" dio: dependency: "direct main" description: name: dio - url: "https://pub.dartlang.org" + sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.0.0" + dotted_border: + dependency: "direct main" + description: + name: dotted_border + sha256: "108837e11848ca776c53b30bc870086f84b62ed6e01c503ed976e8f8c7df9c04" + url: "https://pub.dev" + source: hosted + version: "2.1.0" dropdown_button2: dependency: "direct main" description: name: dropdown_button2 - url: "https://pub.dartlang.org" + sha256: "4458d81bfd24207f3d58f66f78097064e02f810f94cf1bc80bf20fe7685ebc80" + url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "2.0.0" enough_html_editor: dependency: transitive description: path: "." - ref: firebase_integration - resolved-ref: "45560a2d06dd8fac93d6936b9cb835c84f0617a3" + ref: email_supported + resolved-ref: "56d820aa845c98a3a809161c02fe8fff033dd2ae" url: "https://github.com/linagora/enough_html_editor.git" source: git version: "0.0.5" @@ -394,37 +389,66 @@ packages: dependency: transitive description: name: enough_platform_widgets - url: "https://pub.dartlang.org" + sha256: "107921eeeac05218d2a0d8e8c3c302e90aa416cbd528b466d62b77ccaabb359c" + url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.6.0" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" + extended_text: + dependency: "direct main" + description: + name: extended_text + sha256: dec14c9b36d9bbaaf007da5998f5dc72a2dbd5b877601d7b7970bb42524b3ced + url: "https://pub.dev" + source: hosted + version: "11.0.1" + extended_text_library: + dependency: transitive + description: + name: extended_text_library + sha256: c06fbd8e3b6eedadf50cd6c109bbbd80921a6c43e4422d3b4ec9d4cb36ce4555 + url: "https://pub.dev" + source: hosted + version: "11.0.2" + external_app_launcher: + dependency: "direct main" + description: + name: external_app_launcher + sha256: fb55cddd706c62ede11056750d5e018ef379820e09739e967873211dd537d833 + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "3.1.0" external_path: dependency: "direct main" description: name: external_path - url: "https://pub.dartlang.org" + sha256: "2095c626fbbefe70d5a4afc9b1137172a68ee2c276e51c3c1283394485bea8f4" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.3" fading_edge_scrollview: dependency: "direct main" description: name: fading_edge_scrollview - url: "https://pub.dartlang.org" + sha256: c25c2231652ce774cc31824d0112f11f653881f43d7f5302c05af11942052031 + url: "https://pub.dev" source: hosted version: "3.0.0" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" fcm: dependency: "direct main" description: @@ -436,126 +460,158 @@ packages: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" source: hosted version: "2.0.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted version: "6.1.4" file_picker: dependency: "direct main" description: name: file_picker - url: "https://pub.dartlang.org" + sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9 + url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.2.5" filesize: dependency: "direct main" description: name: filesize - url: "https://pub.dartlang.org" + sha256: f53df1f27ff60e466eefcd9df239e02d4722d5e2debee92a87dfd99ac66de2af + url: "https://pub.dev" source: hosted version: "2.0.1" firebase_core: dependency: "direct main" description: name: firebase_core - url: "https://pub.dartlang.org" + sha256: fe30ac230f12f8836bb97e6e09197340d3c584526825b1746ea362a82e1e43f7 + url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.7.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + sha256: "0df0a064ab0cad7f8836291ca6f3272edd7b83ad5b3540478ee46a0849d8022b" + url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.6.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + sha256: "347351a8f0518f3343d79a9a0690fa67ad232fc32e2ea270677791949eac792b" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.3.0" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - url: "https://pub.dartlang.org" + sha256: "95f7565b8e992d2188cdd8dc5612330f7c309485fe425d3f9844f18e90741e3e" + url: "https://pub.dev" source: hosted - version: "14.1.4" + version: "14.2.5" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - url: "https://pub.dartlang.org" + sha256: c5e79e15d1018cafffea1a6e45249db0d6bc42dbe35178634c77488179880e79 + url: "https://pub.dev" source: hosted - version: "4.2.9" + version: "4.2.14" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - url: "https://pub.dartlang.org" + sha256: cd0cfcab7a63282049dec95a9955e7a487b5e580162310d12a82a10c0c32c546 + url: "https://pub.dev" source: hosted - version: "3.2.10" + version: "3.2.15" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" fk_user_agent: dependency: "direct main" description: name: fk_user_agent - url: "https://pub.dartlang.org" + sha256: fd6c94e120786985a292d12f61422a581f4e851148d5940af38b819357b8ad0d + url: "https://pub.dev" source: hosted version: "2.1.0" flex_color_picker: dependency: transitive description: name: flex_color_picker - url: "https://pub.dartlang.org" + sha256: f0e0db8e3e47435cfbe9aa15c71b898fa218be0fc4ae409e1e42d5d5266b2c90 + url: "https://pub.dev" + source: hosted + version: "3.2.0" + flex_seed_scheme: + dependency: transitive + description: + name: flex_seed_scheme + sha256: "7058288ef97d348657ac95cea25d65a9aac181ca08387ede891fd7230ad7600f" + url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "1.2.3" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" + flutter_app_badger: + dependency: "direct main" + description: + name: flutter_app_badger + sha256: "64d4a279bab862ed28850431b9b446b9820aaae0bf363322d51077419f930fa8" + url: "https://pub.dev" + source: hosted + version: "1.5.0" flutter_appauth: dependency: "direct main" description: name: flutter_appauth - url: "https://pub.dartlang.org" + sha256: cbb075ec8fa11d3241cff5e4c20db8aa0732cc0d0bbb3f59b2172d23782dfc0b + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.2.1" flutter_appauth_platform_interface: - dependency: transitive + dependency: "direct main" description: name: flutter_appauth_platform_interface - url: "https://pub.dartlang.org" + sha256: "7b0c6e19e6b9a33098c6a14fb64aca62fccaefd40eccba1c4974d9a9d1fbe95a" + url: "https://pub.dev" source: hosted version: "5.2.0" flutter_appauth_web: dependency: "direct main" description: path: "." - ref: HEAD - resolved-ref: b21b4aed3688dcf9dcb08ebac9e53015f09137bc + ref: main + resolved-ref: f1adeb99cb4e5b0056c2b14ab5dc38ea936e67e1 url: "https://github.com/CarlosPacheco/flutter_appauth_web.git" source: git - version: "0.0.1" + version: "0.0.2" flutter_colorpicker: dependency: transitive description: name: flutter_colorpicker - url: "https://pub.dartlang.org" + sha256: "458a6ed8ea480eb16ff892aedb4b7092b2804affd7e046591fb03127e8d8ef8b" + url: "https://pub.dev" source: hosted version: "1.0.3" flutter_date_range_picker: @@ -563,92 +619,144 @@ packages: description: path: "." ref: master - resolved-ref: c04608f5daad5e96daf0f419946ad0db23c46811 + resolved-ref: "0393f6de405ed6b80ca363e87a436be81e3daf05" url: "https://github.com/linagora/flutter-date-range-picker.git" source: git - version: "1.0.0" + version: "1.0.2" flutter_dotenv: dependency: "direct main" description: name: flutter_dotenv - url: "https://pub.dartlang.org" + sha256: d9283d92059a22e9834bc0a31336658ffba77089fb6f3cc36751f1fc7c6661a3 + url: "https://pub.dev" source: hosted version: "5.0.2" flutter_downloader: dependency: "direct main" description: name: flutter_downloader - url: "https://pub.dartlang.org" + sha256: "6f2836668f33d0cd3ed275c3198d30967c42af53fa9b18c6b0edbf5fc12b599a" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.10.2" flutter_image_compress: dependency: transitive description: name: flutter_image_compress - url: "https://pub.dartlang.org" + sha256: "37f1b26399098e5f97b74c1483f534855e7dff68ead6ddaccf747029fb03f29f" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.3" flutter_inappwebview: dependency: transitive description: name: flutter_inappwebview - url: "https://pub.dartlang.org" + sha256: "6d6c741ddba1dba5229d63ba75767064791a7ce845196b45e31105e93d67c949" + url: "https://pub.dev" source: hosted - version: "5.7.1" - flutter_keyboard_visibility: + version: "6.0.0-beta.22" + flutter_inappwebview_internal_annotations: dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "064a8ccbc76217dcd3b0fd6c6ea6f549e69b2849a0233b5bb46af9632c3ce2ff" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter_keyboard_visibility: + dependency: "direct main" description: name: flutter_keyboard_visibility - url: "https://pub.dartlang.org" + sha256: "4983655c26ab5b959252ee204c2fffa4afeb4413cd030455194ec0caa3b8e7cb" + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.4.1" + flutter_keyboard_visibility_linux: + dependency: transitive + description: + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_keyboard_visibility_platform_interface: dependency: transitive description: name: flutter_keyboard_visibility_platform_interface - url: "https://pub.dartlang.org" + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" source: hosted version: "2.0.0" flutter_keyboard_visibility_web: dependency: transitive description: name: flutter_keyboard_visibility_web - url: "https://pub.dartlang.org" + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" source: hosted version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + sha256: "02dcaf49d405f652b7160e882bacfc02cb497041bb2eab2a49b1c393cf9aac12" + url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.12.0" + flutter_linkify: + dependency: "direct main" + description: + name: flutter_linkify + sha256: "74669e06a8f358fee4512b4320c0b80e51cffc496607931de68d28f099254073" + url: "https://pub.dev" + source: hosted + version: "6.0.0" flutter_lints: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - url: "https://pub.dartlang.org" + sha256: "293995f94e120c8afce768981bd1fa9c5d6de67c547568e3b42ae2defdcbb4a0" + url: "https://pub.dev" source: hosted - version: "12.0.3" + version: "13.0.0" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - url: "https://pub.dartlang.org" + sha256: ccb08b93703aeedb58856e5637450bf3ffec899adb66dc325630b68994734b89 + url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.0+1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - url: "https://pub.dartlang.org" + sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + url: "https://pub.dev" source: hosted version: "6.0.0" flutter_localizations: @@ -660,44 +768,58 @@ packages: dependency: "direct dev" description: name: flutter_native_splash - url: "https://pub.dartlang.org" + sha256: e301ae206ff0fb09b67d3716009c6c28c2da57a0ad164827b178421bb9d601f7 + url: "https://pub.dev" source: hosted - version: "2.2.3+1" + version: "2.2.18" flutter_platform_widgets: dependency: transitive description: name: flutter_platform_widgets - url: "https://pub.dartlang.org" + sha256: b787f001d659ce1fb601332e908efdf44dbedab832387b33162abeed60c1eca2 + url: "https://pub.dev" source: hosted - version: "1.20.0" + version: "3.2.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + sha256: c224ac897bed083dabf11f238dd11a239809b446740be0c2044608c50029ffdf + url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.9" flutter_portal: dependency: "direct main" description: name: flutter_portal - url: "https://pub.dartlang.org" + sha256: ec5bd9a0aa7efeea5a95abf54ce91039c812fe6cc8e30d499eaa45ce387e482e + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.3" + flutter_slidable: + dependency: "direct main" + description: + name: flutter_slidable + sha256: cc4231579e3eae41ae166660df717f4bad1359c87f4a4322ad8ba1befeb3d2be + url: "https://pub.dev" + source: hosted + version: "3.0.0" flutter_staggered_grid_view: dependency: "direct main" description: name: flutter_staggered_grid_view - url: "https://pub.dartlang.org" + sha256: "1312314293acceb65b92754298754801b0e1f26a1845833b740b30415bbbcf07" + url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.6.2" flutter_svg: dependency: "direct main" description: name: flutter_svg - url: "https://pub.dartlang.org" + sha256: "97c5b291b4fd34ae4f55d6a4c05841d4d0ed94952e033c5a6529e1b47b4d2a29" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "2.0.2" flutter_test: dependency: "direct dev" description: flutter @@ -707,35 +829,29 @@ packages: dependency: "direct main" description: name: flutter_typeahead - url: "https://pub.dartlang.org" + sha256: f31211a8536f87908c3dcbdb88666e2f4d77f5f06c2b3a48eaad5599969ff32d + url: "https://pub.dev" source: hosted - version: "3.2.7" + version: "4.6.0" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" - fluttertoast: - dependency: transitive - description: - name: fluttertoast - url: "https://pub.dartlang.org" - source: hosted - version: "8.0.8" - focus_detector: + focus_detector_v2: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: "4b3107b16a93f7c91eed93f0aac1eb1b205f4a45" - url: "https://github.com/ifnyas/focus_detector.git" - source: git - version: "2.0.1" + name: focus_detector_v2 + sha256: "4fed0ad4ef4996711880e26bd8450eb86199acc4f25eafbf49a17d4758f9d139" + url: "https://pub.dev" + source: hosted + version: "3.0.0+2" focused_menu_custom: dependency: "direct main" description: name: focused_menu_custom - url: "https://pub.dartlang.org" + sha256: afbadad1405101f19f651581c569d969ad150df91fbac11d2b13b331bc0ed4ad + url: "https://pub.dev" source: hosted version: "1.2.0" forward: @@ -749,136 +865,153 @@ packages: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.2.0" get: dependency: "direct main" description: name: get - url: "https://pub.dartlang.org" + sha256: "2ba20a47c8f1f233bed775ba2dd0d3ac97b4cf32fc17731b3dfc672b06b0e92a" + url: "https://pub.dev" source: hosted version: "4.6.5" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" source: hosted version: "2.1.1" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" source: hosted version: "2.2.0" hive: dependency: "direct main" description: name: hive - url: "https://pub.dartlang.org" + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.3" hive_generator: dependency: "direct dev" description: name: hive_generator - url: "https://pub.dartlang.org" + sha256: "65998cc4d2cd9680a3d9709d893d2f6bb15e6c1f92626c3f1fa650b4b3281521" + url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.0.0" html: dependency: "direct main" description: name: html - url: "https://pub.dartlang.org" + sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.15.1" html_editor_enhanced: dependency: "direct main" description: path: "." ref: email_supported - resolved-ref: "75cbc35932ec85c0e7f94c17147cf231ea0b812e" + resolved-ref: f9dc6f957500ea2978fe5212512326a5028aee27 url: "https://github.com/linagora/html-editor-enhanced.git" source: git - version: "2.5.0" - html_unescape: - dependency: "direct main" - description: - name: html_unescape - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" + version: "2.5.1" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" source: hosted - version: "0.13.4" + version: "0.13.5" http_mock_adapter: dependency: transitive description: name: http_mock_adapter - url: "https://pub.dartlang.org" + sha256: "0e7eaa5d77a273af1c2b5ec5066578faaa73039b63ccda5263c200756f24441a" + url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.4.2" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted version: "3.2.1" http_parser: dependency: "direct main" description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" image: dependency: transitive description: name: image - url: "https://pub.dartlang.org" + sha256: "483a389d6ccb292b570c31b3a193779b1b0178e7eb571986d9a49904b6861227" + url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.0.15" infinite_listview: dependency: transitive description: name: infinite_listview - url: "https://pub.dartlang.org" + sha256: f6062c1720eb59be553dfa6b89813d3e8dd2f054538445aaa5edaddfa5195ce6 + url: "https://pub.dev" source: hosted version: "1.1.0" + internet_connection_checker: + dependency: "direct main" + description: + name: internet_connection_checker + sha256: "1c683e63e89c9ac66a40748b1b20889fd9804980da732bf2b58d6d5456c8e876" + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" intl: - dependency: transitive + dependency: "direct main" description: name: intl - url: "https://pub.dartlang.org" + sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.0" intl_generator: dependency: "direct main" description: name: intl_generator - url: "https://pub.dartlang.org" + sha256: "120e03ccefe0c215801e44a8ccbaeabe6c895c5792f44298133745ab0a0e0bf5" + url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.4.1" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" jmap_dart_client: dependency: "direct main" description: path: "." ref: master - resolved-ref: "1df4203f3c7cb5f24ee68f3e1060efb7283dd3fd" + resolved-ref: e8005e28b48ee06259d4f51045a58f20c891e0b9 url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" @@ -886,79 +1019,90 @@ packages: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.7" json_annotation: dependency: "direct main" description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.8.0" json_serializable: dependency: "direct dev" description: name: json_serializable - url: "https://pub.dartlang.org" + sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a + url: "https://pub.dev" source: hosted - version: "6.2.0" - lint: + version: "6.6.1" + linkify: dependency: transitive description: - name: lint - url: "https://pub.dartlang.org" + name: linkify + sha256: "4139ea77f4651ab9c315b577da2dd108d9aa0bd84b5d03d33323f1970c645832" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "5.0.0" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.15" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.9.1" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.4" mockito: dependency: "direct dev" description: name: mockito - url: "https://pub.dartlang.org" + sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.4.2" model: dependency: "direct main" description: @@ -970,265 +1114,268 @@ packages: dependency: transitive description: name: nm - url: "https://pub.dartlang.org" + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" source: hosted version: "0.5.0" numberpicker: dependency: transitive description: name: numberpicker - url: "https://pub.dartlang.org" + sha256: "4c129154944b0f6b133e693f8749c3f8bfb67c4d07ef9dcab48b595c22d1f156" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted version: "2.1.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.3" - package_info_plus_linux: - dependency: transitive - description: - name: package_info_plus_linux - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.5" - package_info_plus_macos: - dependency: transitive - description: - name: package_info_plus_macos - url: "https://pub.dartlang.org" + sha256: "8df5ab0a481d7dc20c0e63809e90a588e496d276ba53358afc4c4443d0a00697" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "3.0.3" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - package_info_plus_web: - dependency: transitive - description: - name: package_info_plus_web - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.6" - package_info_plus_windows: - dependency: transitive - description: - name: package_info_plus_windows - url: "https://pub.dartlang.org" + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.0.1" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.8.3" path_drawing: dependency: transitive description: name: path_drawing - url: "https://pub.dartlang.org" + sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977 + url: "https://pub.dev" source: hosted version: "1.0.1" path_parsing: dependency: transitive description: name: path_parsing - url: "https://pub.dartlang.org" + sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + url: "https://pub.dev" source: hosted version: "1.0.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9" + url: "https://pub.dev" source: hosted - version: "2.0.3" - path_provider_linux: + version: "2.0.13" + path_provider_android: dependency: transitive description: - name: path_provider_linux - url: "https://pub.dartlang.org" + name: path_provider_android + sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7" + url: "https://pub.dev" source: hosted - version: "2.1.7" - path_provider_macos: + version: "2.0.24" + path_provider_foundation: dependency: transitive description: - name: path_provider_macos - url: "https://pub.dartlang.org" + name: path_provider_foundation + sha256: "818b2dc38b0f178e0ea3f7cf3b28146faab11375985d815942a68eee11c2d0f7" + url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.2.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + url: "https://pub.dev" + source: hosted + version: "2.1.10" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: c2af5a8a6369992d915f8933dfc23172071001359d17896e83db8be57db8a397 + url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.1" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.5" pattern_formatter: dependency: transitive description: name: pattern_formatter - url: "https://pub.dartlang.org" + sha256: "987d24df31179325897f58258132492e86de577c6ae50c8102b9a1342f607688" + url: "https://pub.dev" source: hosted - version: "2.0.0" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.1" + version: "3.0.0" percent_indicator: dependency: "direct main" description: name: percent_indicator - url: "https://pub.dartlang.org" + sha256: cec41f67181fbd5322aa68b355621d1a4eea827426b8eeb613f6cbe195ff7b4a + url: "https://pub.dev" source: hosted version: "4.2.2" permission_handler: dependency: "direct main" description: name: permission_handler - url: "https://pub.dartlang.org" + sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8" + url: "https://pub.dev" source: hosted version: "10.2.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - url: "https://pub.dartlang.org" + sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2" + url: "https://pub.dev" source: hosted version: "10.2.0" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - url: "https://pub.dartlang.org" + sha256: ee96ac32f5a8e6f80756e25b25b9f8e535816c8e6665a96b6d70681f8c4f7e85 + url: "https://pub.dev" source: hosted - version: "9.0.7" + version: "9.0.8" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - url: "https://pub.dartlang.org" + sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84" + url: "https://pub.dev" source: hosted version: "3.9.0" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - url: "https://pub.dartlang.org" + sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b + url: "https://pub.dev" source: hosted version: "0.1.2" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.0" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pointer_interceptor: dependency: "direct main" description: name: pointer_interceptor - url: "https://pub.dartlang.org" + sha256: "49e6b86ba931d801ce852990d4a8913726ea3964266559e0b058baa3b4408435" + url: "https://pub.dev" source: hosted version: "0.9.1" pointycastle: dependency: transitive description: name: pointycastle - url: "https://pub.dartlang.org" + sha256: c3120a968135aead39699267f4c74bc9a08e4e909e86bc1b0af5bfd78691123c + url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.7.2" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted version: "1.5.1" process: dependency: transitive description: name: process - url: "https://pub.dartlang.org" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" source: hosted version: "4.2.4" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" source: hosted - version: "3.0.1+1" + version: "3.2.1" receive_sharing_intent: dependency: "direct main" description: path: "." ref: master - resolved-ref: e1a945c3da610cdb0ec2788f1b89a96e3f0c6f32 - url: "https://github.com/KasemJaffer/receive_sharing_intent.git" + resolved-ref: f23f7fb0fad25ae80a88350ad8654851f1ee682f + url: "https://github.com/linagora/receive_sharing_intent.git" source: git version: "1.4.5" rich_text_composer: dependency: "direct main" description: path: "." - ref: firebase_integration - resolved-ref: "0250e2d4bbd5bd79c5b5fdca840f0a573f645ac4" + ref: master + resolved-ref: e0603b36b952e97503ca60d773fc35f278a8044a url: "https://github.com/linagora/rich-text-composer.git" source: git - version: "0.0.1" + version: "0.0.2" rule_filter: dependency: "direct main" description: @@ -1240,77 +1387,104 @@ packages: dependency: "direct main" description: name: rxdart - url: "https://pub.dartlang.org" + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" + url: "https://pub.dev" source: hosted - version: "0.27.3" - share: + version: "0.27.7" + share_plus: dependency: "direct main" description: - name: share - url: "https://pub.dartlang.org" + name: share_plus + sha256: "8c6892037b1824e2d7e8f59d54b3105932899008642e6372e5079c6939b4b625" + url: "https://pub.dev" + source: hosted + version: "6.3.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "82ddd4ab9260c295e6e39612d4ff00390b9a7a21f1bb1da771e2f232d80ab8a1" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "3.2.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://pub.dartlang.org" + sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41 + url: "https://pub.dev" source: hosted - version: "2.0.5" - shared_preferences_linux: + version: "2.0.18" + shared_preferences_android: dependency: transitive description: - name: shared_preferences_linux - url: "https://pub.dartlang.org" + name: shared_preferences_android + sha256: "8304d8a1f7d21a429f91dee552792249362b68a331ac5c3c1caf370f658873f6" + url: "https://pub.dev" source: hosted - version: "2.1.2" - shared_preferences_macos: + version: "2.1.0" + shared_preferences_foundation: dependency: transitive description: - name: shared_preferences_macos - url: "https://pub.dartlang.org" + name: shared_preferences_foundation + sha256: cf2a42fb20148502022861f71698db12d937c7459345a1bdaa88fc91a91b3603 + url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.2.0" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" + url: "https://pub.dev" + source: hosted + version: "2.2.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://pub.dartlang.org" + sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://pub.dartlang.org" + sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.1.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://pub.dartlang.org" + sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.2.0" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted version: "1.0.3" simple_observable: dependency: transitive description: name: simple_observable - url: "https://pub.dartlang.org" + sha256: b392795c48f8b5f301b4c8f73e15f56e38fe70f42278c649d8325e859a783301 + url: "https://pub.dev" source: hosted version: "2.0.0" sky_engine: @@ -1322,319 +1496,347 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298 + url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.2.7" source_helper: dependency: transitive description: name: source_helper - url: "https://pub.dartlang.org" + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" source: hosted version: "1.3.3" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.8.2" - sqflite: - dependency: transitive - description: - name: sqflite - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0+4" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - url: "https://pub.dartlang.org" - source: hosted - version: "2.4.0" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" super_tag_editor: dependency: "direct main" description: path: "." ref: master - resolved-ref: df99b9536d92fc94d5137aa118f84c1cbb9c0c11 + resolved-ref: "6e066b29659c6d3a2f20686219bb131164ab2e51" url: "https://github.com/dab246/super_tag_editor.git" source: git - version: "0.0.3+4" + version: "0.2.0" syncfusion_flutter_core: dependency: transitive description: name: syncfusion_flutter_core - url: "https://pub.dartlang.org" + sha256: "3979f0b1c5a97422cadae52d476c21fa3e0fb671ef51de6cae1d646d8b99fe1f" + url: "https://pub.dev" source: hosted - version: "20.4.43" + version: "20.4.54" syncfusion_flutter_datepicker: dependency: transitive description: name: syncfusion_flutter_datepicker - url: "https://pub.dartlang.org" - source: hosted - version: "20.3.48" - synchronized: - dependency: transitive - description: - name: synchronized - url: "https://pub.dartlang.org" + sha256: "5e7bfe47437c1a106f8c4502a13ab7d57ed8195c1578b40ffa50dea1120e26e3" + url: "https://pub.dev" source: hosted - version: "3.0.0+3" + version: "20.4.51" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.5.1" textfield_tags: dependency: "direct main" description: name: textfield_tags - url: "https://pub.dartlang.org" + sha256: c1d215f481e7e8da5c79719825e595db4f829bf1ad3fce4c7ce43d340aa72683 + url: "https://pub.dev" source: hosted version: "2.0.2" timeago: dependency: "direct main" description: name: timeago - url: "https://pub.dartlang.org" + sha256: "4addcda362e51f23cf7ae2357fccd053f29d59b4ddd17fb07fc3e7febb47a456" + url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.5.0" timezone: dependency: transitive description: name: timezone - url: "https://pub.dartlang.org" + sha256: "24c8fcdd49a805d95777a39064862133ff816ebfffe0ceff110fb5960e557964" + url: "https://pub.dev" source: hosted version: "0.9.1" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" universal_html: - dependency: transitive + dependency: "direct main" description: name: universal_html - url: "https://pub.dartlang.org" + sha256: b5061c64c7c863c12e46279e032976f1c274f927fb3589b52b5928dcd2d52f7c + url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "2.0.9" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.dartlang.org" + sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.0" uri: dependency: transitive description: name: uri - url: "https://pub.dartlang.org" + sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" + url: "https://pub.dev" source: hosted version: "1.0.0" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: "75f2846facd11168d007529d6cd8fcb2b750186bea046af9711f10b907e1587e" + url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.1.10" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: dd729390aa936bf1bdf5cd1bc7468ff340263f80a2c4f569416507667de8e3c8 + url: "https://pub.dev" source: hosted - version: "6.0.22" + version: "6.0.26" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: "3dedc66ca3c0bef9e6a93c0999aee102556a450afcc1b7bcfeace7a424927d92" + url: "https://pub.dev" source: hosted - version: "6.0.17" + version: "6.1.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: "206fb8334a700ef7754d6a9ed119e7349bc830448098f21a69bf1b4ed038cabc" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.4" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "0ef2b4f97942a16523e51256b799e9aa1843da6c60c55eefbfa9dbc2dcb8331a" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.4" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "6c9ca697a5ae218ce56cece69d46128169a58aa8653c1b01d26fcd4aad8c4370" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" + url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.16" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: a83ba3607a507758669cfafb03f9de09bf6e6280c14d9b9cb18f013e406dcacd + url: "https://pub.dev" + source: hosted + version: "3.0.5" + url_strategy: + dependency: "direct main" + description: + name: url_strategy + sha256: "42b68b42a9864c4d710401add17ad06e28f1c1d5500c93b98c431f6b0ea4ab87" + url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "0.2.0" uuid: dependency: "direct main" description: name: uuid - url: "https://pub.dartlang.org" + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.7" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "4cf8e60dbe4d3a693d37dff11255a172594c0793da542183cbfe7fe978ae4aaa" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "278ad5f816f58b1967396d1f78ced470e3e58c9fe4b27010102c0a595c764468" + url: "https://pub.dev" + source: hosted + version: "1.1.4" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: "0bf61ad56e6fd6688a2865d3ceaea396bc6a0a90ea0d7ad5049b1b76c09d6163" + url: "https://pub.dev" + source: hosted + version: "1.1.4" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" visibility_detector: dependency: transitive description: name: visibility_detector - url: "https://pub.dartlang.org" + sha256: "15c54a459ec2c17b4705450483f3d5a2858e733aee893dcee9d75fd04814940d" + url: "https://pub.dev" source: hosted version: "0.3.3" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" source: hosted version: "2.3.0" - webview_flutter: - dependency: transitive - description: - name: webview_flutter - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - webview_flutter_android: - dependency: transitive - description: - name: webview_flutter_android - url: "https://pub.dartlang.org" - source: hosted - version: "2.10.4" - webview_flutter_platform_interface: - dependency: transitive - description: - name: webview_flutter_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.9.5" - webview_flutter_wkwebview: - dependency: transitive - description: - name: webview_flutter_wkwebview - url: "https://pub.dartlang.org" - source: hosted - version: "2.9.5" win32: dependency: transitive description: name: win32 - url: "https://pub.dartlang.org" + sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "3.1.3" worker_manager: dependency: "direct main" description: name: worker_manager - url: "https://pub.dartlang.org" + sha256: "42501e49ee0acad9eeda562984e3dcfe6fe3d26f2d8dc410bd76308a86447eb5" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + workmanager: + dependency: "direct main" + description: + name: workmanager + sha256: e0be7e35d644643f164ee45d2ce14414f0e0fdde19456aa66065f35a0b1d2ea1 + url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "0.5.1" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + url: "https://pub.dev" source: hosted version: "0.2.0+3" xml: dependency: "direct dev" description: name: xml - url: "https://pub.dartlang.org" + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.2" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=3.0.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index e279acdcf5..f95ead156d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -15,10 +15,10 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.7.0 +version: 0.9.3 environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: @@ -45,196 +45,200 @@ dependencies: fcm: path: fcm + ### Dependencies from git ### rich_text_composer: git: url: https://github.com/linagora/rich-text-composer.git - ref: firebase_integration - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + ref: master + + html_editor_enhanced: + git: + url: https://github.com/linagora/html-editor-enhanced.git + ref: email_supported + + jmap_dart_client: + git: + url: https://github.com/linagora/jmap-dart-client.git + ref: master + + contacts_service: + git: + url: https://github.com/linagora/flutter_contacts.git + ref: master + + receive_sharing_intent: + git: + url: https://github.com/linagora/receive_sharing_intent.git + ref: master + + flutter_appauth_web: + git: + url: https://github.com/CarlosPacheco/flutter_appauth_web.git + ref: main + + flutter_date_range_picker: + git: + url: https://github.com/linagora/flutter-date-range-picker.git + ref: master + + super_tag_editor: + git: + url: https://github.com/dab246/super_tag_editor.git + ref: master + + ### Dependencies from pub.dev ### + cupertino_icons: 1.0.5 - # GetX get: 4.6.5 - # intl - intl_generator: 0.3.0 + intl_generator: 0.4.1 - # flutter_svg - flutter_svg: 1.1.0 + flutter_svg: 2.0.2 - # Http client - dio: 4.0.6 + dio: 5.0.0 - # equatable - equatable: 2.0.3 + equatable: 2.0.5 - # shared_preferences - shared_preferences: 2.0.5 + shared_preferences: 2.0.18 - # either dartz: 0.10.1 - # flutter_dotenv flutter_dotenv: 5.0.2 - # flutter_typeahead - flutter_typeahead: 3.2.7 - built_collection: 5.1.1 - textfield_tags: ^2.0.1 + textfield_tags: 2.0.2 - # jmap_dart_client - jmap_dart_client: - git: - url: https://github.com/linagora/jmap-dart-client.git - ref: master + http_parser: 4.0.2 - # http_parser - http_parser: 4.0.0 + collection: 1.17.1 - collection: 1.16.0 - - # file_size filesize: 2.0.1 - # super_tag_editor - super_tag_editor: - git: - url: https://github.com/dab246/super_tag_editor.git - ref: master - # uuid - uuid: 3.0.4 + uuid: 3.0.7 - # flutter_downloader - flutter_downloader: 1.7.0 + flutter_downloader: 1.10.2 - # external_path - external_path: 1.0.1 + external_path: 1.0.3 - # path_provider - path_provider: 2.0.3 + path_provider: 2.0.13 - # device_info_plus - device_info_plus: 4.0.2 + device_info_plus: 8.1.0 - # permission_handler permission_handler: 10.2.0 - # share - share: 2.0.4 + share_plus: 6.3.1 - # flutter_contacts - contacts_service: - git: - url: https://github.com/linagora/flutter_contacts.git - ref: master - - # file_picker - file_picker: 5.0.1 - - # hive - hive: 2.0.4 + file_picker: 5.2.5 - # html_editor_enhanced: Compose email for Web Browser - html_editor_enhanced: - git: - url: https://github.com/linagora/html-editor-enhanced.git - ref: email_supported + hive: 2.2.3 - # fk_user_agent fk_user_agent: 2.1.0 pointer_interceptor: 0.9.1 - rxdart: 0.27.3 + rxdart: 0.27.7 - connectivity_plus: 2.2.1 + connectivity_plus: 3.0.3 - package_info_plus: 1.4.3 + package_info_plus: 3.0.3 - receive_sharing_intent: - git: - url: https://github.com/KasemJaffer/receive_sharing_intent.git - ref: master - - dropdown_button2: 1.4.0 - - flutter_staggered_grid_view: 0.6.1 + dropdown_button2: 2.0.0 - flutter_portal: 1.1.0 + flutter_staggered_grid_view: 0.6.2 - timeago: 3.2.2 + flutter_portal: 1.1.3 - better_open_file: 3.6.3 + timeago: 3.5.0 - # OIDC Mobile - flutter_appauth: 4.0.0 + better_open_file: 3.6.4 - # OIDC Web - flutter_appauth_web: - git: - url: https://github.com/CarlosPacheco/flutter_appauth_web.git + flutter_appauth: 4.2.1 percent_indicator: 4.2.2 - # Isolate - worker_manager: 4.4.0 + worker_manager: 5.0.3 - async: 2.8.2 + async: 2.11.0 - # HTML5 parser - html: 0.15.0 + html: 0.15.1 custom_pop_up_menu: 1.2.4 byte_converter: 1.3.0 - json_annotation: 4.5.0 + json_annotation: 4.8.0 - flutter_date_range_picker: - git: - url: https://github.com/linagora/flutter-date-range-picker.git - ref: master + url_launcher: 6.1.10 - url_launcher: 6.1.5 + firebase_core: 2.7.0 - firebase_core: 2.4.0 + firebase_messaging: 14.2.5 - firebase_messaging: 14.1.4 + flutter_local_notifications: 13.0.0 - flutter_local_notifications: 12.0.3 + fading_edge_scrollview: 3.0.0 - # https://pub.dev/packages/focus_detector - # Use for flutter 3.0.5 https://github.com/EdsonBueno/focus_detector/pull/14 - focus_detector: - git: https://github.com/ifnyas/focus_detector.git + focused_menu_custom: 1.2.0 - html_unescape: 2.0.0 + focus_detector_v2: 3.0.0+2 - fading_edge_scrollview: 3.0.0 + universal_html: 2.0.9 - focused_menu_custom: 1.2.0 + debounce_throttle: 2.0.0 + + flutter_appauth_platform_interface: 5.2.0 + + intl: 0.18.0 + + external_app_launcher: 3.1.0 + + workmanager: 0.5.1 + + flutter_typeahead: 4.6.0 + + flutter_keyboard_visibility: 5.4.1 + + extended_text: 11.0.1 + + flutter_app_badger: 1.5.0 + + date_format: 2.0.7 + + flutter_linkify: 6.0.0 + + flutter_slidable: 3.0.0 + + url_strategy: 0.2.0 + + dotted_border: 2.1.0 + + internet_connection_checker: 1.0.0+1 dev_dependencies: flutter_test: sdk: flutter - build_runner: 2.1.11 + build_runner: 2.3.3 - mockito: 5.2.0 + mockito: 5.4.2 - hive_generator: 1.1.3 + hive_generator: 2.0.0 - flutter_lints: 1.0.4 + flutter_lints: 2.0.1 - flutter_launcher_icons: 0.9.3 + flutter_native_splash: 2.2.18 - flutter_native_splash: 2.2.3+1 + flutter_launcher_icons: 0.12.0 - xml: 6.1.0 + xml: 6.2.2 + + json_serializable: 6.6.1 + +dependency_overrides: + + pointer_interceptor: 0.9.1 - json_serializable: 6.2.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -305,4 +309,7 @@ flutter_native_splash: color: "#000000" branding: "assets/icons/icon_branding.png" image: "assets/icons/icon_logo.png" - web: false \ No newline at end of file + +cider: + link_template: + tag: https://github.com/linagora/tmail-flutter/releases/tag/v%tag% # initial release link template \ No newline at end of file diff --git a/rule_filter/analysis_options.yaml b/rule_filter/analysis_options.yaml index a5744c1cfb..195df97751 100644 --- a/rule_filter/analysis_options.yaml +++ b/rule_filter/analysis_options.yaml @@ -1,4 +1,16 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. include: package:flutter_lints/flutter.yaml -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +linter: + rules: + constant_identifier_names: false + non_constant_identifier_names: false + unnecessary_string_escapes: false diff --git a/rule_filter/pubspec.lock b/rule_filter/pubspec.lock index f89b485ab6..dffc02fb4b 100644 --- a/rule_filter/pubspec.lock +++ b/rule_filter/pubspec.lock @@ -5,198 +5,218 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.dev" source: hosted - version: "47.0.0" + version: "61.0.0" analyzer: dependency: transitive description: name: analyzer - url: "https://pub.dartlang.org" + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "5.13.0" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" build: dependency: transitive description: name: build - url: "https://pub.dartlang.org" + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" source: hosted version: "2.3.1" build_config: dependency: transitive description: name: build_config - url: "https://pub.dartlang.org" + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" build_daemon: dependency: transitive description: name: build_daemon - url: "https://pub.dartlang.org" + sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" build_resolvers: dependency: transitive description: name: build_resolvers - url: "https://pub.dartlang.org" + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + url: "https://pub.dev" source: hosted - version: "2.0.10" + version: "2.2.0" build_runner: dependency: "direct dev" description: name: build_runner - url: "https://pub.dartlang.org" + sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.3.3" build_runner_core: dependency: transitive description: name: build_runner_core - url: "https://pub.dartlang.org" + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" source: hosted version: "7.2.7" built_collection: dependency: transitive description: name: built_collection - url: "https://pub.dartlang.org" + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" source: hosted version: "5.1.1" built_value: dependency: transitive description: name: built_value - url: "https://pub.dartlang.org" + sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" + url: "https://pub.dev" source: hosted - version: "8.4.2" + version: "8.4.4" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.0" checked_yaml: dependency: transitive description: name: checked_yaml - url: "https://pub.dartlang.org" + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" code_builder: dependency: transitive description: name: code_builder - url: "https://pub.dartlang.org" + sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.5.0" collection: dependency: transitive description: name: collection - url: "https://pub.dartlang.org" + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.1" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.1" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" source: hosted version: "3.0.2" dart_style: dependency: transitive description: name: dart_style - url: "https://pub.dartlang.org" + sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352" + url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" dartz: dependency: transitive description: name: dartz - url: "https://pub.dartlang.org" + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" source: hosted version: "0.10.1" dio: - dependency: transitive + dependency: "direct main" description: name: dio - url: "https://pub.dartlang.org" + sha256: "9fdbf71baeb250fc9da847f6cb2052196f62c19906a3657adfc18631a667d316" + url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.0.0" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" source: hosted version: "6.1.4" fixnum: dependency: transitive description: name: fixnum - url: "https://pub.dartlang.org" + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -206,9 +226,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - url: "https://pub.dartlang.org" + sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c + url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.1" flutter_test: dependency: "direct dev" description: flutter @@ -218,57 +239,64 @@ packages: dependency: transitive description: name: frontend_server_client - url: "https://pub.dartlang.org" + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "3.2.0" glob: dependency: transitive description: name: glob - url: "https://pub.dartlang.org" + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" graphs: dependency: transitive description: name: graphs - url: "https://pub.dartlang.org" + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" source: hosted version: "2.2.0" http_mock_adapter: dependency: "direct main" description: name: http_mock_adapter - url: "https://pub.dartlang.org" + sha256: "0e7eaa5d77a273af1c2b5ec5066578faaa73039b63ccda5263c200756f24441a" + url: "https://pub.dev" source: hosted - version: "0.3.2" + version: "0.4.2" http_multi_server: dependency: transitive description: name: http_multi_server - url: "https://pub.dartlang.org" + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" source: hosted version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" io: dependency: transitive description: name: io - url: "https://pub.dartlang.org" + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" jmap_dart_client: dependency: "direct main" description: path: "." ref: master - resolved-ref: "45ea109f70be0d868b005aadf11a39e2ac816c38" + resolved-ref: e8005e28b48ee06259d4f51045a58f20c891e0b9 url: "https://github.com/linagora/jmap-dart-client.git" source: git version: "0.0.1" @@ -276,128 +304,146 @@ packages: dependency: transitive description: name: js - url: "https://pub.dartlang.org" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: "direct main" description: name: json_annotation - url: "https://pub.dartlang.org" + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.8.0" json_serializable: dependency: "direct dev" description: name: json_serializable - url: "https://pub.dartlang.org" + sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a + url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.6.1" lints: dependency: transitive description: name: lints - url: "https://pub.dartlang.org" + sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.1" logging: dependency: transitive description: name: logging - url: "https://pub.dartlang.org" + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.15" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 + url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.2.0" meta: dependency: transitive description: name: meta - url: "https://pub.dartlang.org" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + url: "https://pub.dev" source: hosted - version: "1.7.0" + version: "1.9.1" mime: dependency: transitive description: name: mime - url: "https://pub.dartlang.org" + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.4" mockito: dependency: "direct dev" description: name: mockito - url: "https://pub.dartlang.org" + sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.4.2" package_config: dependency: transitive description: name: package_config - url: "https://pub.dartlang.org" + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" source: hosted version: "2.1.0" path: dependency: transitive description: name: path - url: "https://pub.dartlang.org" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.8.3" pool: dependency: transitive description: name: pool - url: "https://pub.dartlang.org" + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" source: hosted version: "1.5.1" pub_semver: dependency: transitive description: name: pub_semver - url: "https://pub.dartlang.org" + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" pubspec_parse: dependency: transitive description: name: pubspec_parse - url: "https://pub.dartlang.org" + sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" source: hosted - version: "3.0.1+1" + version: "3.2.1" shelf: dependency: transitive description: name: shelf - url: "https://pub.dartlang.org" + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" source: hosted version: "1.4.0" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - url: "https://pub.dartlang.org" + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.3" sky_engine: dependency: transitive description: flutter @@ -407,107 +453,122 @@ packages: dependency: transitive description: name: source_gen - url: "https://pub.dartlang.org" + sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298 + url: "https://pub.dev" source: hosted - version: "1.2.6" + version: "1.2.7" source_helper: dependency: transitive description: name: source_helper - url: "https://pub.dartlang.org" + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" source: hosted version: "1.3.3" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 + url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.9.1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" stream_transform: dependency: transitive description: name: stream_transform - url: "https://pub.dartlang.org" + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb + url: "https://pub.dev" source: hosted - version: "0.4.9" + version: "0.5.1" timing: dependency: transitive description: name: timing - url: "https://pub.dartlang.org" + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.1" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" source: hosted version: "1.3.1" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" watcher: dependency: transitive description: name: watcher - url: "https://pub.dartlang.org" + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" source: hosted version: "1.0.2" web_socket_channel: dependency: transitive description: name: web_socket_channel - url: "https://pub.dartlang.org" + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" source: hosted version: "3.1.1" sdks: - dart: ">=2.17.0 <3.0.0" - flutter: ">=2.5.0" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.0.0" diff --git a/rule_filter/pubspec.yaml b/rule_filter/pubspec.yaml index bdaff3e556..3406395aad 100644 --- a/rule_filter/pubspec.yaml +++ b/rule_filter/pubspec.yaml @@ -5,34 +5,39 @@ publish_to: 'none' version: 1.0.0+1 environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: sdk: flutter - equatable: 2.0.3 - - json_annotation: 4.5.0 - + ### Dependencies from git ### jmap_dart_client: git: url: https://github.com/linagora/jmap-dart-client.git ref: master - http_mock_adapter: 0.3.2 + ### Dependencies from pub.dev ### + equatable: 2.0.5 + + json_annotation: 4.8.0 + + dio: 5.0.0 + + http_mock_adapter: 0.4.2 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: 1.0.4 + flutter_lints: 2.0.1 + + build_runner: 2.3.3 - build_runner: 2.1.11 + json_serializable: 6.6.1 - json_serializable: 6.2.0 + mockito: 5.4.2 - mockito: 5.2.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/rule_filter/test/datasource/rule_filter_datasource_impl_test.dart b/rule_filter/test/datasource/rule_filter_datasource_impl_test.dart index 664179a055..dd9eebba8d 100644 --- a/rule_filter/test/datasource/rule_filter_datasource_impl_test.dart +++ b/rule_filter/test/datasource/rule_filter_datasource_impl_test.dart @@ -40,20 +40,20 @@ void main() { group('rule_filter_datasource_impl_test', () { - late RuleFilterAPI _ruleFilterAPI; - late RuleFilterDataSourceImpl _ruleFilterDataSourceImpl; + late RuleFilterAPI ruleFilterAPI; + late RuleFilterDataSourceImpl ruleFilterDataSourceImpl; setUp(() { - _ruleFilterAPI = MockRuleFilterAPI(); - _ruleFilterDataSourceImpl = RuleFilterDataSourceImpl(_ruleFilterAPI); + ruleFilterAPI = MockRuleFilterAPI(); + ruleFilterDataSourceImpl = RuleFilterDataSourceImpl(ruleFilterAPI); }); test('getListTMailRule should return success with valid data', () async { - when(_ruleFilterAPI.getListTMailRule(AccountId(Id( + when(ruleFilterAPI.getListTMailRule(AccountId(Id( '0eacc7a5c74b27ab36a823bc5c34da36e16c093705f241d6ed5f48ee73a4ecfb')))) .thenAnswer((_) async => expectRuleFilter1.rules); - final result = await _ruleFilterDataSourceImpl.getListTMailRule(AccountId(Id( + final result = await ruleFilterDataSourceImpl.getListTMailRule(AccountId(Id( '0eacc7a5c74b27ab36a823bc5c34da36e16c093705f241d6ed5f48ee73a4ecfb'))); expect(result, expectRuleFilter1.rules); }); diff --git a/rule_filter/test/rule_filter/get_rule_filter_method_test.dart b/rule_filter/test/rule_filter/get_rule_filter_method_test.dart index bafa1be0ea..7d226a9f6e 100644 --- a/rule_filter/test/rule_filter/get_rule_filter_method_test.dart +++ b/rule_filter/test/rule_filter/get_rule_filter_method_test.dart @@ -95,8 +95,7 @@ void main() { }, headers: { "accept": "application/json;jmapVersion=rfc-8621", - "content-type": "application/json; charset=utf-8", - "content-length": 182 + "content-length": 250 }); final httpClient = HttpClient(dio); diff --git a/rule_filter/test/rule_filter/set_rule_filter_method_test.dart b/rule_filter/test/rule_filter/set_rule_filter_method_test.dart index 26b65ee665..3aed41597b 100644 --- a/rule_filter/test/rule_filter/set_rule_filter_method_test.dart +++ b/rule_filter/test/rule_filter/set_rule_filter_method_test.dart @@ -104,8 +104,7 @@ void main() { }, headers: { "accept": "application/json;jmapVersion=rfc-8621", - "content-type": "application/json; charset=utf-8", - "content-length": 340 + "content-length": 753 }); final setRuleFilterMethod = SetRuleFilterMethod(AccountId(Id( diff --git a/security.md b/security.md new file mode 100644 index 0000000000..ff35cd9b02 --- /dev/null +++ b/security.md @@ -0,0 +1,45 @@ +# Security policy + +We believe in transparency to mitigate security risks. All known vulnerabilities are available on +our [security page](docs/modules/ROOT/pages/security.adoc). + +We disclose such security issues only once a released version addressing the issue is available. + +We use automated tools to review our docker images and dependencies. + +# Reporting security vulnerabilities + +To ensure safety of our users, security process needs to happen privately. + +Here are the steps: + + - 1. Reporter email the issues privately to `openpaas-james[AT]linagora.com`. + - 2. We will then evaluate the validity of your report, and write back to you within two weeks. This response time + accounts for vacation and will generally be quicker. + - 3. We will propose a fix that we will review with you. This can take up to two weeks. + - 4. We will propose a draft for the announcement that we will review with you. + - 5. We will propose you a schedule for the release and the announcements. + - 6. One week after the release we will disclose the vulnerability. + +You will be credited in the vulnerability report for your findings. + +# Threat model + +The following threats are generic points of attention for email softwares: + + - Virusses: Emails are one of the vector for spreading virusses. We recommend administrators to set up virus scans +as part of their email infrastructure. We recommend user to be cautious opening attachments of unknown senders or +suspicious emails. We recommend users to have an anti-virus installed. For instance [TeamMail backend](https://github.com/linagora/tmail-backend/) is integrated with +[ClamAV](https://www.clamav.net/) solution. + - Fishing: Attackers can try trick users on their identity and try to make them believe they are a legitimate sender and try to use +this to either make user conduct actions or leak information. We recommend administrator to run an anti-Spam software, to verify SPF and DKIM +records. For instance [TeamMail backend](https://github.com/linagora/tmail-backend/) is integrated with [RSpamD](https://rspamd.com/) solution. + - `Authentication`. We recommend administrator to set up strong authentication with an OIDC provider. This avoids TeamMail frontend to store directly user credentials, +enable configuring handy features like two factor authentication and the like. For instance [TeamMail backend](https://github.com/linagora/tmail-backend/) is integrated +with [LemonLDAP](https://lemonldap-ng.org/) identity provider using the [Apisix](https://apisix.apache.org/) API gateway. We also recommend users to logout once they finished using TeamMail. + - HTML rendering. By design, email embeds HTML generated by non trusted sources. TeamMail do sanitize HTML prior displays. The use of the canvas also limits the +impact of HTML rendering related vulnerabilities. Loading of remote resources like images can also be used for tracking purposes. As off today, TeamMail do not allow +blocking such tracking attempts. + +TeamMail today relies on [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging) for its push architecture. +Only StateChanges are transiting though a third party, which to not include presonnal data. diff --git a/server/nginx.conf b/server/nginx.conf index 8073c41575..4ec91c0373 100644 --- a/server/nginx.conf +++ b/server/nginx.conf @@ -22,8 +22,10 @@ http { #tcp_nopush on; keepalive_timeout 65; + add_header Cache-Control "no-cache"; + etag on; - include /etc/nginx/conf.d/*.conf; + # include /etc/nginx/conf.d/*.conf; server { listen 80; diff --git a/test/features/caching/email_cache_client_test.dart b/test/features/caching/email_cache_client_test.dart index 3e6fb935b7..e7350cd78f 100644 --- a/test/features/caching/email_cache_client_test.dart +++ b/test/features/caching/email_cache_client_test.dart @@ -3,45 +3,45 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/caching/clients/email_cache_client.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; -import 'package:tmail_ui_user/features/caching/email_cache_client.dart'; import 'package:tmail_ui_user/features/thread/data/extensions/email_extension.dart'; import '../../fixtures/email_fixtures.dart'; void main() { - late EmailCacheClient _emailCacheClient; + late EmailCacheClient emailCacheClient; setUpAll(() { HiveCacheConfig().setUp(cachePath: Directory.current.path); }); setUp(() { - _emailCacheClient = EmailCacheClient(); + emailCacheClient = EmailCacheClient(); }); group('[delete]', () { test('cache should delete item successfully when cache empty', () async { - await _emailCacheClient.deleteItem(EmailFixtures.email1.id.toString()); + await emailCacheClient.deleteItem(EmailFixtures.email1.id.toString()); - final remainingItems = await _emailCacheClient.getAll(); + final remainingItems = await emailCacheClient.getAll(); expect(remainingItems.length, equals(0)); }); test('cache should not delete item which not in the list', () async { - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email1.id.toString(), EmailFixtures.email1.toEmailCache()); - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email2.id.toString(), EmailFixtures.email2.toEmailCache()); - await _emailCacheClient.deleteItem(EmailFixtures.email3.id.toString()); + await emailCacheClient.deleteItem(EmailFixtures.email3.id.toString()); - final remainingItems = await _emailCacheClient.getAll(); + final remainingItems = await emailCacheClient.getAll(); expect(remainingItems.length, equals(2)); expect( @@ -53,35 +53,35 @@ void main() { }); test('cache should delete item successfully', () async { - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email1.id.toString(), EmailFixtures.email1.toEmailCache()); - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email2.id.toString(), EmailFixtures.email2.toEmailCache()); - await _emailCacheClient.deleteItem(EmailFixtures.email1.id.toString()); + await emailCacheClient.deleteItem(EmailFixtures.email1.id.toString()); - final remainingItems = await _emailCacheClient.getAll(); + final remainingItems = await emailCacheClient.getAll(); expect(remainingItems.length, equals(1)); expect(remainingItems.first, equals(EmailFixtures.email2.toEmailCache())); }); test('cache should not delete item twice', () async { - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email1.id.toString(), EmailFixtures.email1.toEmailCache()); - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email2.id.toString(), EmailFixtures.email2.toEmailCache()); - await _emailCacheClient.deleteItem(EmailFixtures.email1.id.toString()); - await _emailCacheClient.deleteItem(EmailFixtures.email1.id.toString()); + await emailCacheClient.deleteItem(EmailFixtures.email1.id.toString()); + await emailCacheClient.deleteItem(EmailFixtures.email1.id.toString()); - final remainingItems = await _emailCacheClient.getAll(); + final remainingItems = await emailCacheClient.getAll(); expect(remainingItems.length, equals(1)); expect(remainingItems.first, equals(EmailFixtures.email2.toEmailCache())); @@ -90,26 +90,26 @@ void main() { group('[add]', () { test('cache should add item when cache empty', () async { - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email1.id.toString(), EmailFixtures.email1.toEmailCache()); - final remainingItems = await _emailCacheClient.getAll(); + final remainingItems = await emailCacheClient.getAll(); expect(remainingItems.length, equals(1)); expect(remainingItems.first, equals(EmailFixtures.email1.toEmailCache())); }); test('cache should add item when cache not empty', () async { - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email1.id.toString(), EmailFixtures.email1.toEmailCache()); - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email2.id.toString(), EmailFixtures.email2.toEmailCache()); - final remainingItems = await _emailCacheClient.getAll(); + final remainingItems = await emailCacheClient.getAll(); expect(remainingItems.length, equals(2)); expect( @@ -121,15 +121,15 @@ void main() { }); test('cache should not add item twice', () async { - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email1.id.toString(), EmailFixtures.email1.toEmailCache()); - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email1.id.toString(), EmailFixtures.email1.toEmailCache()); - final remainingItems = await _emailCacheClient.getAll(); + final remainingItems = await emailCacheClient.getAll(); expect(remainingItems.length, equals(1)); expect( @@ -142,30 +142,30 @@ void main() { group('[update]', () { test('cache should update item when update to iem which not in cache', () async { - await _emailCacheClient.updateItem( + await emailCacheClient.updateItem( EmailFixtures.email1.id.toString(), EmailFixtures.email1.toEmailCache()); - final remainingItems = await _emailCacheClient.getAll(); + final remainingItems = await emailCacheClient.getAll(); expect(remainingItems.length, equals(1)); expect(remainingItems.first, equals(EmailFixtures.email1.toEmailCache())); }); test('cache should update correctly item', () async { - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email1.id.toString(), EmailFixtures.email1.toEmailCache()); - await _emailCacheClient.insertItem( + await emailCacheClient.insertItem( EmailFixtures.email2.id.toString(), EmailFixtures.email2.toEmailCache()); - await _emailCacheClient.updateItem( + await emailCacheClient.updateItem( EmailFixtures.email1.id.toString(), EmailFixtures.email3.toEmailCache()); - final remainingItems = await _emailCacheClient.getAll(); + final remainingItems = await emailCacheClient.getAll(); expect(remainingItems.length, equals(2)); expect( @@ -178,6 +178,6 @@ void main() { }); tearDown(() async { - await _emailCacheClient.deleteBox(); + await emailCacheClient.deleteBox(); }); } \ No newline at end of file diff --git a/test/features/caching/mailbox_cache_client_test.dart b/test/features/caching/mailbox_cache_client_test.dart index 6663f6c9bf..11ce1fd018 100644 --- a/test/features/caching/mailbox_cache_client_test.dart +++ b/test/features/caching/mailbox_cache_client_test.dart @@ -3,45 +3,45 @@ import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/caching/clients/mailbox_cache_client.dart'; import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; -import 'package:tmail_ui_user/features/caching/mailbox_cache_client.dart'; import 'package:tmail_ui_user/features/mailbox/data/extensions/mailbox_extension.dart'; import '../../fixtures/mailbox_fixtures.dart'; void main() { - late MailboxCacheClient _mailboxCacheClient; + late MailboxCacheClient mailboxCacheClient; setUpAll(() { HiveCacheConfig().setUp(cachePath: Directory.current.path); }); setUp(() { - _mailboxCacheClient = MailboxCacheClient(); + mailboxCacheClient = MailboxCacheClient(); }); group('[delete]', () { test('cache should delete item successfully when cache empty', () async { - await _mailboxCacheClient.deleteItem(MailboxFixtures.inboxMailbox.id.toString()); + await mailboxCacheClient.deleteItem(MailboxFixtures.inboxMailbox.id.toString()); - final remainingItems = await _mailboxCacheClient.getAll(); + final remainingItems = await mailboxCacheClient.getAll(); expect(remainingItems.length, equals(0)); }); test('cache should not delete item which not in the list', () async { - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.inboxMailbox.id.toString(), MailboxFixtures.inboxMailbox.toMailboxCache()); - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.sentMailbox.id.toString(), MailboxFixtures.sentMailbox.toMailboxCache()); - await _mailboxCacheClient.deleteItem(MailboxFixtures.folder1.id.toString()); + await mailboxCacheClient.deleteItem(MailboxFixtures.folder1.id.toString()); - final remainingItems = await _mailboxCacheClient.getAll(); + final remainingItems = await mailboxCacheClient.getAll(); expect(remainingItems.length, equals(2)); expect( @@ -53,35 +53,35 @@ void main() { }); test('cache should delete item successfully', () async { - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.inboxMailbox.id.toString(), MailboxFixtures.inboxMailbox.toMailboxCache()); - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.sentMailbox.id.toString(), MailboxFixtures.sentMailbox.toMailboxCache()); - await _mailboxCacheClient.deleteItem(MailboxFixtures.inboxMailbox.id.toString()); + await mailboxCacheClient.deleteItem(MailboxFixtures.inboxMailbox.id.toString()); - final remainingItems = await _mailboxCacheClient.getAll(); + final remainingItems = await mailboxCacheClient.getAll(); expect(remainingItems.length, equals(1)); expect(remainingItems[0], equals(MailboxFixtures.sentMailbox.toMailboxCache())); }); test('cache should not delete item twice', () async { - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.inboxMailbox.id.toString(), MailboxFixtures.inboxMailbox.toMailboxCache()); - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.sentMailbox.id.toString(), MailboxFixtures.sentMailbox.toMailboxCache()); - await _mailboxCacheClient.deleteItem(MailboxFixtures.inboxMailbox.id.toString()); - await _mailboxCacheClient.deleteItem(MailboxFixtures.inboxMailbox.id.toString()); + await mailboxCacheClient.deleteItem(MailboxFixtures.inboxMailbox.id.toString()); + await mailboxCacheClient.deleteItem(MailboxFixtures.inboxMailbox.id.toString()); - final remainingItems = await _mailboxCacheClient.getAll(); + final remainingItems = await mailboxCacheClient.getAll(); expect(remainingItems.length, equals(1)); expect(remainingItems[0], equals(MailboxFixtures.sentMailbox.toMailboxCache())); @@ -90,26 +90,26 @@ void main() { group('[add]', () { test('cache should add item when cache empty', () async { - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.inboxMailbox.id.toString(), MailboxFixtures.inboxMailbox.toMailboxCache()); - final remainingItems = await _mailboxCacheClient.getAll(); + final remainingItems = await mailboxCacheClient.getAll(); expect(remainingItems.length, equals(1)); expect(remainingItems[0], equals(MailboxFixtures.inboxMailbox.toMailboxCache())); }); test('cache should add item when cache not empty', () async { - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.inboxMailbox.id.toString(), MailboxFixtures.inboxMailbox.toMailboxCache()); - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.sentMailbox.id.toString(), MailboxFixtures.sentMailbox.toMailboxCache()); - final remainingItems = await _mailboxCacheClient.getAll(); + final remainingItems = await mailboxCacheClient.getAll(); expect(remainingItems.length, equals(2)); expect( @@ -121,15 +121,15 @@ void main() { }); test('cache should not add item twice', () async { - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.inboxMailbox.id.toString(), MailboxFixtures.inboxMailbox.toMailboxCache()); - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.inboxMailbox.id.toString(), MailboxFixtures.inboxMailbox.toMailboxCache()); - final remainingItems = await _mailboxCacheClient.getAll(); + final remainingItems = await mailboxCacheClient.getAll(); expect(remainingItems.length, equals(1)); expect( @@ -142,30 +142,30 @@ void main() { group('[update]', () { test('cache should update item when update to iem which not in cache', () async { - await _mailboxCacheClient.updateItem( + await mailboxCacheClient.updateItem( MailboxFixtures.inboxMailbox.id.toString(), MailboxFixtures.inboxMailbox.toMailboxCache()); - final remainingItems = await _mailboxCacheClient.getAll(); + final remainingItems = await mailboxCacheClient.getAll(); expect(remainingItems.length, equals(1)); expect(remainingItems[0], equals(MailboxFixtures.inboxMailbox.toMailboxCache())); }); test('cache should update correctly item', () async { - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.inboxMailbox.id.toString(), MailboxFixtures.inboxMailbox.toMailboxCache()); - await _mailboxCacheClient.insertItem( + await mailboxCacheClient.insertItem( MailboxFixtures.sentMailbox.id.toString(), MailboxFixtures.sentMailbox.toMailboxCache()); - await _mailboxCacheClient.updateItem( + await mailboxCacheClient.updateItem( MailboxFixtures.inboxMailbox.id.toString(), MailboxFixtures.folder1.toMailboxCache()); - final remainingItems = await _mailboxCacheClient.getAll(); + final remainingItems = await mailboxCacheClient.getAll(); expect(remainingItems.length, equals(2)); expect( @@ -178,6 +178,6 @@ void main() { }); tearDown(() async { - await _mailboxCacheClient.deleteBox(); + await mailboxCacheClient.deleteBox(); }); } \ No newline at end of file diff --git a/test/features/manage_account/identity/identity_utils_test.dart b/test/features/manage_account/identity/identity_utils_test.dart index cfe1ff3bd0..ec7c2e8d5b 100644 --- a/test/features/manage_account/identity/identity_utils_test.dart +++ b/test/features/manage_account/identity/identity_utils_test.dart @@ -4,12 +4,12 @@ import 'package:jmap_dart_client/jmap/identities/identity.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/profiles/identities/utils/identity_utils.dart'; void main() { - final _identityUtils = IdentityUtils(); + final identityUtils = IdentityUtils(); group('get smallest sortOrder identity test', () { test('getSmallestOrderedIdentity return identity with sortOrder equals to 1', () { final sortOrders = [1,99, 999, 100, 201]; final listIdentities1 = sortOrders.map((e) => Identity(sortOrder: UnsignedInt(e))).toList(); - final actual1 = _identityUtils.getSmallestOrderedIdentity(listIdentities1); + final actual1 = identityUtils.getSmallestOrderedIdentity(listIdentities1); expect(actual1, [listIdentities1[0]]); }); @@ -17,7 +17,7 @@ void main() { test('getSmallestOrderedIdentity return list identities with sortOrder equals to 1', () { final sortOrders = [1, 1, 23, 2, 3, 4]; final listIdentities1 = sortOrders.map((e) => Identity(sortOrder: UnsignedInt(e))).toList(); - final actual1 = _identityUtils.getSmallestOrderedIdentity(listIdentities1); + final actual1 = identityUtils.getSmallestOrderedIdentity(listIdentities1); expect(actual1, [listIdentities1[0], listIdentities1[1]]); }); @@ -30,7 +30,7 @@ void main() { } return Identity(); }).toList(); - final actual1 = _identityUtils.getSmallestOrderedIdentity(listIdentities1); + final actual1 = identityUtils.getSmallestOrderedIdentity(listIdentities1); expect(actual1, [listIdentities1[1]]); }); @@ -43,7 +43,7 @@ void main() { } return Identity(); }).toList(); - final actual1 = _identityUtils.getSmallestOrderedIdentity(listIdentities1); + final actual1 = identityUtils.getSmallestOrderedIdentity(listIdentities1); expect(actual1, [listIdentities1[2]]); }); @@ -56,7 +56,7 @@ void main() { } return Identity(); }).toList(); - final actual1 = _identityUtils.getSmallestOrderedIdentity(listIdentities1); + final actual1 = identityUtils.getSmallestOrderedIdentity(listIdentities1); expect(actual1!.length, 5); expect(actual1[1], Identity(sortOrder: null)); @@ -65,12 +65,9 @@ void main() { test('getSmallestOrderedIdentity return list identities with sortOrder equals to 0', () { final sortOrders = [0, 0, 0, 0 ,0 ,0]; final listIdentities1 = sortOrders.map((e) { - if(e != null){ - return Identity(sortOrder: UnsignedInt(e)); - } - return Identity(); + return Identity(sortOrder: UnsignedInt(e)); }).toList(); - final actual1 = _identityUtils.getSmallestOrderedIdentity(listIdentities1); + final actual1 = identityUtils.getSmallestOrderedIdentity(listIdentities1); expect(actual1!.length, 6); expect(actual1[1], Identity(sortOrder: UnsignedInt(0))); @@ -79,7 +76,7 @@ void main() { test('getSmallestOrderedIdentity return identity with sortOrder equals to 0', () { final sortOrders = [0, 99, 999, 100, 12]; final listIdentities1 = sortOrders.map((e) => Identity(sortOrder: UnsignedInt(e))).toList(); - final actual1 = _identityUtils.getSmallestOrderedIdentity(listIdentities1); + final actual1 = identityUtils.getSmallestOrderedIdentity(listIdentities1); expect(actual1, [listIdentities1[0]]); }); @@ -89,7 +86,7 @@ void main() { test('should return a list of identities with increasing sortOrder of identities', () { final sortOrders = [9, 1, 99, 98, 95, 0, 12]; final listIdentities1 = sortOrders.map((e) => Identity(sortOrder: UnsignedInt(e))).toList(); - _identityUtils.sortListIdentities(listIdentities1); + identityUtils.sortListIdentities(listIdentities1); expect(listIdentities1, [0, 1, 9, 12, 95, 98, 99].map((e) => Identity(sortOrder: UnsignedInt(e))).toList()); }); @@ -101,7 +98,7 @@ void main() { } return Identity(); }).toList(); - _identityUtils.sortListIdentities(listIdentities1); + identityUtils.sortListIdentities(listIdentities1); expect(listIdentities1, [1, 9, 12, 95, 98, 99, null].map((e) => Identity(sortOrder: e != null ? UnsignedInt(e) : null)).toList()); }); @@ -113,7 +110,7 @@ void main() { } return Identity(); }).toList(); - _identityUtils.sortListIdentities(listIdentities1); + identityUtils.sortListIdentities(listIdentities1); expect(listIdentities1, [999, null, null, null, null, null, null].map((e) => Identity(sortOrder: e != null ? UnsignedInt(e) : null)).toList()); }); @@ -125,7 +122,7 @@ void main() { } return Identity(); }).toList(); - _identityUtils.sortListIdentities(listIdentities1); + identityUtils.sortListIdentities(listIdentities1); expect(listIdentities1, [null, null, null, null, null, null, null].map((e) => Identity()).toList()); }); }); diff --git a/test/features/offline_mode/mailbox_state_worker_queue.dart b/test/features/offline_mode/mailbox_state_worker_queue.dart new file mode 100644 index 0000000000..e159daacef --- /dev/null +++ b/test/features/offline_mode/mailbox_state_worker_queue.dart @@ -0,0 +1,9 @@ + + +import 'package:tmail_ui_user/features/offline_mode/hive_worker/hive_worker_queue.dart'; + +class MailboxStateWorkerQueue extends WorkerQueue { + + @override + String get workerName => 'MailboxStateWorkerQueue'; +} \ No newline at end of file diff --git a/test/features/offline_mode/woker_queue_test.dart b/test/features/offline_mode/woker_queue_test.dart new file mode 100644 index 0000000000..eab62ef5e5 --- /dev/null +++ b/test/features/offline_mode/woker_queue_test.dart @@ -0,0 +1,70 @@ +@TestOn('vm') + +import 'dart:io'; + +import 'package:core/utils/app_logger.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:tmail_ui_user/features/caching/config/hive_cache_config.dart'; +import 'package:tmail_ui_user/features/caching/clients/state_cache_client.dart'; +import 'package:tmail_ui_user/features/mailbox/data/extensions/state_extension.dart'; +import 'package:tmail_ui_user/features/mailbox/data/model/state_type.dart'; +import 'package:tmail_ui_user/features/offline_mode/hive_worker/hive_task.dart'; + +import 'mailbox_state_worker_queue.dart'; + +void main() { + late StateCacheClient stateCacheClient; + late MailboxStateWorkerQueue workerQueue; + + setUpAll(() { + HiveCacheConfig().setUp(cachePath: Directory.current.path); + }); + + setUp(() { + stateCacheClient = StateCacheClient(); + workerQueue = MailboxStateWorkerQueue(); + }); + + HiveTask generateTask(String key, State value) { + return HiveTask( + id: key, + runnable: () async { + final stateCache = value.toStateCache(StateType.mailbox); + await stateCacheClient.insertItem(key, stateCache); + }, + conditionInvoked: () async { + final listState = await stateCacheClient.getAll(); + return listState.length < 2; + } + ); + } + + group('mailbox_state_worker_queue test', () { + test('stateCacheClient should contain only 2 elements when performing 4 more tasks in the queue at the same time.', () async { + await stateCacheClient.clearAllData(); + final stateCacheStart = await stateCacheClient.getAll(); + log('main():mailbox_state_worker_queue:stateCacheStart: $stateCacheStart | Length: ${stateCacheStart.length}'); + await Future.delayed(const Duration(seconds: 5)); + + await Future.wait([ + workerQueue.addTask(generateTask('Task1', State('value1'))), + workerQueue.addTask(generateTask('Task2', State('value2'))), + workerQueue.addTask(generateTask('Task3', State('value3'))), + workerQueue.addTask(generateTask('Task4', State('value4'))) + ], eagerError: true); + + await Future.delayed(const Duration(seconds: 5)); + + await workerQueue.release(); + + final stateCacheEnd = await stateCacheClient.getAll(); + log('main():mailbox_state_worker_queue::stateCacheEnd: $stateCacheEnd | Length: ${stateCacheEnd.length}'); + expect(stateCacheEnd.length, equals(2)); + }); + }); + + tearDown(() async { + await stateCacheClient.deleteBox(); + }); +} \ No newline at end of file diff --git a/test/features/session/session_hive_obj_test.dart b/test/features/session/session_hive_obj_test.dart new file mode 100644 index 0000000000..a88b0102dc --- /dev/null +++ b/test/features/session/session_hive_obj_test.dart @@ -0,0 +1,16 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/features/session/data/extensions/session_hive_obj_extension.dart'; +import 'package:tmail_ui_user/features/session/domain/extensions/session_extensions.dart'; + +import '../../fixtures/session_fixtures.dart'; + +void main() { + group('SessionHiveObj test', () { + test('toSession() should return parsing correctly session', () { + final sessionHiveObj = SessionFixtures.aliceSession.toHiveObj(); + final sessionParsed = sessionHiveObj.toSession(); + + expect(sessionParsed.accounts.length, equals(SessionFixtures.aliceSession.accounts.length)); + }); + }); +} \ No newline at end of file diff --git a/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.dart b/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.dart index 3e058158fd..72f78d0c5e 100644 --- a/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.dart +++ b/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:core/core.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator_property.dart'; import 'package:model/model.dart'; @@ -21,6 +20,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/get_emails_in_mail import '../../../../fixtures/account_fixtures.dart'; import '../../../../fixtures/email_fixtures.dart'; import '../../../../fixtures/mailbox_fixtures.dart'; +import '../../../../fixtures/session_fixtures.dart'; import 'get_emails_in_mailbox_interactor_test.mocks.dart'; @GenerateMocks([ThreadRepository]) @@ -37,6 +37,7 @@ void main() { test('getEmailsInMailboxInteractor should execute to get all email from cache and network', () async { when(threadRepository.getAllEmail( + SessionFixtures.aliceSession, AccountFixtures.aliceAccountId, limit: UnsignedInt(20), sort: {}..add(EmailComparator(EmailComparatorProperty.sentAt)..setIsAscending(false)), @@ -65,6 +66,7 @@ void main() { })); final streamStates = getEmailsInMailboxInteractor.execute( + SessionFixtures.aliceSession, AccountFixtures.aliceAccountId, limit: UnsignedInt(20), sort: {}..add(EmailComparator(EmailComparatorProperty.sentAt)..setIsAscending(false)), @@ -79,7 +81,7 @@ void main() { expect(states.length, equals(3)); expect(states, containsAllInOrder({ - Right(LoadingState()), + Right(GetAllEmailLoading()), Right(GetAllEmailSuccess( emailList: { EmailFixtures.email1.toPresentationEmail(), diff --git a/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.mocks.dart b/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.mocks.dart deleted file mode 100644 index cb5e7f139d..0000000000 --- a/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.mocks.dart +++ /dev/null @@ -1,112 +0,0 @@ -// Mocks generated by Mockito 5.2.0 from annotations -// in tmail_ui_user/test/features/thread/domain/usecases/get_emails_in_mailbox_interactor_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i3; - -import 'package:jmap_dart_client/jmap/account_id.dart' as _i5; -import 'package:jmap_dart_client/jmap/core/filter/filter.dart' as _i13; -import 'package:jmap_dart_client/jmap/core/properties/properties.dart' as _i9; -import 'package:jmap_dart_client/jmap/core/sort/comparator.dart' as _i7; -import 'package:jmap_dart_client/jmap/core/state.dart' as _i10; -import 'package:jmap_dart_client/jmap/core/unsigned_int.dart' as _i6; -import 'package:jmap_dart_client/jmap/mail/email/email.dart' as _i12; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart' as _i14; -import 'package:mockito/mockito.dart' as _i1; -import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart' - as _i8; -import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart' - as _i4; -import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart' - as _i11; -import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart' - as _i2; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types - -/// A class which mocks [ThreadRepository]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockThreadRepository extends _i1.Mock implements _i2.ThreadRepository { - MockThreadRepository() { - _i1.throwOnMissingStub(this); - } - - @override - _i3.Stream<_i4.EmailsResponse> getAllEmail(_i5.AccountId? accountId, - {_i6.UnsignedInt? limit, - Set<_i7.Comparator>? sort, - _i8.EmailFilter? emailFilter, - _i9.Properties? propertiesCreated, - _i9.Properties? propertiesUpdated}) => - (super.noSuchMethod( - Invocation.method(#getAllEmail, [ - accountId - ], { - #limit: limit, - #sort: sort, - #emailFilter: emailFilter, - #propertiesCreated: propertiesCreated, - #propertiesUpdated: propertiesUpdated - }), - returnValue: Stream<_i4.EmailsResponse>.empty()) - as _i3.Stream<_i4.EmailsResponse>); - @override - _i3.Stream<_i4.EmailsResponse> refreshChanges( - _i5.AccountId? accountId, _i10.State? currentState, - {Set<_i7.Comparator>? sort, - _i8.EmailFilter? emailFilter, - _i9.Properties? propertiesCreated, - _i9.Properties? propertiesUpdated}) => - (super.noSuchMethod( - Invocation.method(#refreshChanges, [ - accountId, - currentState - ], { - #sort: sort, - #emailFilter: emailFilter, - #propertiesCreated: propertiesCreated, - #propertiesUpdated: propertiesUpdated - }), - returnValue: Stream<_i4.EmailsResponse>.empty()) - as _i3.Stream<_i4.EmailsResponse>); - @override - _i3.Stream<_i4.EmailsResponse> loadMoreEmails( - _i11.GetEmailRequest? emailRequest) => - (super.noSuchMethod(Invocation.method(#loadMoreEmails, [emailRequest]), - returnValue: Stream<_i4.EmailsResponse>.empty()) - as _i3.Stream<_i4.EmailsResponse>); - @override - _i3.Future> searchEmails(_i5.AccountId? accountId, - {_i6.UnsignedInt? limit, - Set<_i7.Comparator>? sort, - _i13.Filter? filter, - _i9.Properties? properties}) => - (super.noSuchMethod( - Invocation.method(#searchEmails, [ - accountId - ], { - #limit: limit, - #sort: sort, - #filter: filter, - #properties: properties - }), - returnValue: Future>.value(<_i12.Email>[])) - as _i3.Future>); - @override - _i3.Future> emptyTrashFolder( - _i5.AccountId? accountId, _i14.MailboxId? trashMailboxId) => - (super.noSuchMethod( - Invocation.method(#emptyTrashFolder, [accountId, trashMailboxId]), - returnValue: Future>.value(<_i12.EmailId>[])) - as _i3.Future>); -} diff --git a/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.dart b/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.dart index 1c4fcee8b8..fd9c54d72b 100644 --- a/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.dart +++ b/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:core/core.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator.dart'; import 'package:jmap_dart_client/jmap/mail/email/email_comparator_property.dart'; import 'package:model/model.dart'; @@ -19,6 +18,7 @@ import 'package:tmail_ui_user/features/thread/domain/usecases/refresh_changes_em import '../../../../fixtures/account_fixtures.dart'; import '../../../../fixtures/email_fixtures.dart'; import '../../../../fixtures/mailbox_fixtures.dart'; +import '../../../../fixtures/session_fixtures.dart'; import '../../../../fixtures/state_fixtures.dart'; import 'refresh_changes_emails_in_mailbox_interactor_test.mocks.dart'; @@ -36,9 +36,10 @@ void main() { test('refreshChangesEmailsInMailboxInteractor should execute to get changes email from cache and network', () async { when(threadRepository.refreshChanges( + SessionFixtures.aliceSession, AccountFixtures.aliceAccountId, StateFixtures.currentEmailState, - sort: Set()..add(EmailComparator(EmailComparatorProperty.sentAt)..setIsAscending(false)), + sort: {}..add(EmailComparator(EmailComparatorProperty.sentAt)..setIsAscending(false)), propertiesCreated: ThreadConstants.propertiesDefault, propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, emailFilter: EmailFilter( @@ -56,9 +57,10 @@ void main() { })); final streamStates = refreshChangesEmailsInMailboxInteractor.execute( + SessionFixtures.aliceSession, AccountFixtures.aliceAccountId, StateFixtures.currentEmailState, - sort: Set()..add(EmailComparator(EmailComparatorProperty.sentAt)..setIsAscending(false)), + sort: {}..add(EmailComparator(EmailComparatorProperty.sentAt)..setIsAscending(false)), propertiesCreated: ThreadConstants.propertiesDefault, propertiesUpdated: ThreadConstants.propertiesUpdatedDefault, emailFilter: EmailFilter( @@ -69,7 +71,7 @@ void main() { expect(states.length, equals(2)); expect(states, containsAllInOrder({ - Right(RefreshingState()), + Right(RefreshChangesAllEmailLoading()), Right(RefreshChangesAllEmailSuccess( emailList: { EmailFixtures.email1.toPresentationEmail(), diff --git a/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.mocks.dart b/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.mocks.dart deleted file mode 100644 index d55f1162c2..0000000000 --- a/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.mocks.dart +++ /dev/null @@ -1,112 +0,0 @@ -// Mocks generated by Mockito 5.2.0 from annotations -// in tmail_ui_user/test/features/thread/domain/usecases/refresh_changes_emails_in_mailbox_interactor_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i3; - -import 'package:jmap_dart_client/jmap/account_id.dart' as _i5; -import 'package:jmap_dart_client/jmap/core/filter/filter.dart' as _i13; -import 'package:jmap_dart_client/jmap/core/properties/properties.dart' as _i9; -import 'package:jmap_dart_client/jmap/core/sort/comparator.dart' as _i7; -import 'package:jmap_dart_client/jmap/core/state.dart' as _i10; -import 'package:jmap_dart_client/jmap/core/unsigned_int.dart' as _i6; -import 'package:jmap_dart_client/jmap/mail/email/email.dart' as _i12; -import 'package:jmap_dart_client/jmap/mail/mailbox/mailbox.dart' as _i14; -import 'package:mockito/mockito.dart' as _i1; -import 'package:tmail_ui_user/features/thread/domain/model/email_filter.dart' - as _i8; -import 'package:tmail_ui_user/features/thread/domain/model/email_response.dart' - as _i4; -import 'package:tmail_ui_user/features/thread/domain/model/get_email_request.dart' - as _i11; -import 'package:tmail_ui_user/features/thread/domain/repository/thread_repository.dart' - as _i2; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types - -/// A class which mocks [ThreadRepository]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockThreadRepository extends _i1.Mock implements _i2.ThreadRepository { - MockThreadRepository() { - _i1.throwOnMissingStub(this); - } - - @override - _i3.Stream<_i4.EmailsResponse> getAllEmail(_i5.AccountId? accountId, - {_i6.UnsignedInt? limit, - Set<_i7.Comparator>? sort, - _i8.EmailFilter? emailFilter, - _i9.Properties? propertiesCreated, - _i9.Properties? propertiesUpdated}) => - (super.noSuchMethod( - Invocation.method(#getAllEmail, [ - accountId - ], { - #limit: limit, - #sort: sort, - #emailFilter: emailFilter, - #propertiesCreated: propertiesCreated, - #propertiesUpdated: propertiesUpdated - }), - returnValue: Stream<_i4.EmailsResponse>.empty()) - as _i3.Stream<_i4.EmailsResponse>); - @override - _i3.Stream<_i4.EmailsResponse> refreshChanges( - _i5.AccountId? accountId, _i10.State? currentState, - {Set<_i7.Comparator>? sort, - _i8.EmailFilter? emailFilter, - _i9.Properties? propertiesCreated, - _i9.Properties? propertiesUpdated}) => - (super.noSuchMethod( - Invocation.method(#refreshChanges, [ - accountId, - currentState - ], { - #sort: sort, - #emailFilter: emailFilter, - #propertiesCreated: propertiesCreated, - #propertiesUpdated: propertiesUpdated - }), - returnValue: Stream<_i4.EmailsResponse>.empty()) - as _i3.Stream<_i4.EmailsResponse>); - @override - _i3.Stream<_i4.EmailsResponse> loadMoreEmails( - _i11.GetEmailRequest? emailRequest) => - (super.noSuchMethod(Invocation.method(#loadMoreEmails, [emailRequest]), - returnValue: Stream<_i4.EmailsResponse>.empty()) - as _i3.Stream<_i4.EmailsResponse>); - @override - _i3.Future> searchEmails(_i5.AccountId? accountId, - {_i6.UnsignedInt? limit, - Set<_i7.Comparator>? sort, - _i13.Filter? filter, - _i9.Properties? properties}) => - (super.noSuchMethod( - Invocation.method(#searchEmails, [ - accountId - ], { - #limit: limit, - #sort: sort, - #filter: filter, - #properties: properties - }), - returnValue: Future>.value(<_i12.Email>[])) - as _i3.Future>); - @override - _i3.Future> emptyTrashFolder( - _i5.AccountId? accountId, _i14.MailboxId? trashMailboxId) => - (super.noSuchMethod( - Invocation.method(#emptyTrashFolder, [accountId, trashMailboxId]), - returnValue: Future>.value(<_i12.EmailId>[])) - as _i3.Future>); -} \ No newline at end of file diff --git a/test/fixtures/email_fixtures.dart b/test/fixtures/email_fixtures.dart index 401418a40e..41e0d10dff 100644 --- a/test/fixtures/email_fixtures.dart +++ b/test/fixtures/email_fixtures.dart @@ -8,7 +8,7 @@ import 'mailbox_fixtures.dart'; class EmailFixtures { static final email1 = Email( - EmailId(Id("382312d0-fa5c-11eb-b647-2fef1ee78d9e")), + id: EmailId(Id("382312d0-fa5c-11eb-b647-2fef1ee78d9e")), preview: "Dear QA,I attached image here", hasAttachment: false, subject: "test inline image", @@ -21,7 +21,7 @@ class EmailFixtures { ); static final email2 = Email( - EmailId(Id("bc8a5320-fa58-11eb-b647-2fef1ee78d9e")), + id: EmailId(Id("bc8a5320-fa58-11eb-b647-2fef1ee78d9e")), preview: "This event is about to begin Noti check TimeFriday 23 October 2020 12:00 - 12:30 Europe/Paris (See in Calendar)Location1 thai ha (See in Map)Attendees - User A (Organizer) - Lê Nguyễn - User C (Organizer) - User A Resourc", hasAttachment: false, subject: "Notification: Recurrencr", @@ -46,7 +46,7 @@ class EmailFixtures { ); static final email4 = Email( - EmailId(Id("d9b3b880-fa6f-11eb-b647-2fef1ee78d9e")), + id: EmailId(Id("d9b3b880-fa6f-11eb-b647-2fef1ee78d9e")), preview: "alo -- desktop signature", hasAttachment: true, subject: "test attachment", @@ -59,7 +59,7 @@ class EmailFixtures { ); static final email5 = Email( - EmailId(Id("637f1ef0-fa5d-11eb-b647-2fef1ee78d9e")), + id: EmailId(Id("637f1ef0-fa5d-11eb-b647-2fef1ee78d9e")), preview: "Dear, test inline Thanks and BRs-- desktop signature", hasAttachment: false, subject: "test inline image", diff --git a/test/fixtures/session_fixtures.dart b/test/fixtures/session_fixtures.dart new file mode 100644 index 0000000000..fdf5b4dab9 --- /dev/null +++ b/test/fixtures/session_fixtures.dart @@ -0,0 +1,104 @@ +import 'package:jmap_dart_client/jmap/account_id.dart'; +import 'package:jmap_dart_client/jmap/core/account/account.dart'; +import 'package:jmap_dart_client/jmap/core/capability/capability_identifier.dart'; +import 'package:jmap_dart_client/jmap/core/capability/core_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/default_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/mail_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/mdn_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/submission_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/vacation_capability.dart'; +import 'package:jmap_dart_client/jmap/core/capability/websocket_capability.dart'; +import 'package:jmap_dart_client/jmap/core/id.dart'; +import 'package:jmap_dart_client/jmap/core/session/session.dart'; +import 'package:jmap_dart_client/jmap/core/sort/collation_identifier.dart'; +import 'package:jmap_dart_client/jmap/core/state.dart'; +import 'package:jmap_dart_client/jmap/core/unsigned_int.dart'; +import 'package:jmap_dart_client/jmap/core/user_name.dart'; + +class SessionFixtures { + static final aliceSession = Session( + { + CapabilityIdentifier.jmapSubmission: SubmissionCapability( + maxDelayedSend: UnsignedInt(0), + submissionExtensions: {} + ), + CapabilityIdentifier.jmapCore: CoreCapability( + maxSizeUpload: UnsignedInt(20971520), + maxConcurrentUpload: UnsignedInt(4), + maxSizeRequest: UnsignedInt(10000000), + maxConcurrentRequests: UnsignedInt(4), + maxCallsInRequest: UnsignedInt(16), + maxObjectsInGet: UnsignedInt(500), + maxObjectsInSet: UnsignedInt(500), + collationAlgorithms: {CollationIdentifier("i;unicode-casemap")} + ), + CapabilityIdentifier.jmapMail: MailCapability( + maxMailboxesPerEmail: UnsignedInt(10000000), + maxSizeAttachmentsPerEmail: UnsignedInt(20000000), + emailQuerySortOptions: {"receivedAt", "sentAt", "size", "from", "to", "subject"}, + mayCreateTopLevelMailbox: true + ), + CapabilityIdentifier.jmapWebSocket: WebSocketCapability( + supportsPush: true, + url: Uri.parse('ws://domain.com/jmap/ws') + ), + CapabilityIdentifier(Uri.parse('urn:apache:james:params:jmap:mail:quota')): DefaultCapability({}), + CapabilityIdentifier(Uri.parse('urn:apache:james:params:jmap:mail:shares')): DefaultCapability({}), + CapabilityIdentifier.jmapVacationResponse: VacationCapability(), + CapabilityIdentifier.jmapMdn: MdnCapability(), + }, + { + AccountId(Id('29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')): Account( + AccountName('bob@domain.tld'), + true, + false, + { + CapabilityIdentifier.jmapSubmission: SubmissionCapability( + maxDelayedSend: UnsignedInt(0), + submissionExtensions: {} + ), + CapabilityIdentifier.jmapWebSocket: WebSocketCapability( + supportsPush: true, + url: Uri.parse('ws://domain.com/jmap/ws') + ), + CapabilityIdentifier.jmapCore: CoreCapability( + maxSizeUpload: UnsignedInt(20971520), + maxConcurrentUpload: UnsignedInt(4), + maxSizeRequest: UnsignedInt(10000000), + maxConcurrentRequests: UnsignedInt(4), + maxCallsInRequest: UnsignedInt(16), + maxObjectsInGet: UnsignedInt(500), + maxObjectsInSet: UnsignedInt(500), + collationAlgorithms: {CollationIdentifier("i;unicode-casemap")} + ), + CapabilityIdentifier.jmapMail: MailCapability( + maxMailboxesPerEmail: UnsignedInt(10000000), + maxSizeAttachmentsPerEmail: UnsignedInt(20000000), + emailQuerySortOptions: {"receivedAt", "sentAt", "size", "from", "to", "subject"}, + mayCreateTopLevelMailbox: true + ), + CapabilityIdentifier(Uri.parse('urn:apache:james:params:jmap:mail:quota')): DefaultCapability({}), + CapabilityIdentifier(Uri.parse('urn:apache:james:params:jmap:mail:shares')): DefaultCapability({}), + CapabilityIdentifier.jmapVacationResponse: VacationCapability(), + CapabilityIdentifier.jmapMdn: MdnCapability() + } + ) + }, + { + CapabilityIdentifier.jmapSubmission: AccountId(Id('29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')), + CapabilityIdentifier.jmapWebSocket: AccountId(Id('29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')), + CapabilityIdentifier.jmapCore: AccountId(Id('29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')), + CapabilityIdentifier.jmapMail: AccountId(Id('29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')), + CapabilityIdentifier(Uri.parse('urn:apache:james:params:jmap:mail:quota')): AccountId(Id('29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')), + CapabilityIdentifier(Uri.parse('urn:apache:james:params:jmap:mail:shares')): AccountId(Id('29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')), + CapabilityIdentifier.jmapVacationResponse: AccountId(Id('29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')), + CapabilityIdentifier.jmapMdn: AccountId(Id('29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')), + }, + UserName('bob@domain.tld'), + Uri.parse('http://domain.com/jmap'), + Uri.parse('http://domain.com/download/{accountId}/{blobId}/?type={type}&name={name}'), + Uri.parse('http://domain.com/upload/{accountId}'), + Uri.parse('http://domain.com/eventSource?types={types}&closeAfter={closeafter}&ping={ping}'), + State('2c9f1b12-b35a-43e6-9af2-0106fb53a943') + ); +} \ No newline at end of file diff --git a/test/main/error/capability_validator_test.dart b/test/main/error/capability_validator_test.dart index 7d088ac3d3..34b43dab58 100644 --- a/test/main/error/capability_validator_test.dart +++ b/test/main/error/capability_validator_test.dart @@ -24,17 +24,20 @@ void main() { test('when session missing one capability exception should throw', () { final session = Session( { - CapabilityIdentifier.jmapSubmission: - SubmissionCapability(UnsignedInt(0), {}), + CapabilityIdentifier.jmapSubmission: SubmissionCapability( + maxDelayedSend: UnsignedInt(0), + submissionExtensions: {} + ), CapabilityIdentifier.jmapMail: MailCapability( - UnsignedInt(10000000), - null, - UnsignedInt(200), - UnsignedInt(20000000), - {"receivedAt", "sentAt", "size", "from", "to", "subject"}, - true), - CapabilityIdentifier.jmapWebSocket: - WebSocketCapability(true, Uri.parse('ws://domain.com/jmap/ws')), + maxMailboxesPerEmail: UnsignedInt(10000000), + maxSizeAttachmentsPerEmail: UnsignedInt(20000000), + emailQuerySortOptions: {"receivedAt", "sentAt", "size", "from", "to", "subject"}, + mayCreateTopLevelMailbox: true + ), + CapabilityIdentifier.jmapWebSocket: WebSocketCapability( + supportsPush: true, + url: Uri.parse('ws://domain.com/jmap/ws') + ), CapabilityIdentifier( Uri.parse('urn:apache:james:params:jmap:mail:quota')): DefaultCapability({}), @@ -45,20 +48,21 @@ void main() { CapabilityIdentifier.jmapMdn: MdnCapability() }, { - AccountId(Id( - '29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')): - Account(AccountName('bob@domain.tld'), true, false, { - CapabilityIdentifier.jmapSubmission: - SubmissionCapability(UnsignedInt(0), {}), + AccountId(Id('29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')): Account(AccountName('bob@domain.tld'), true, false, { + CapabilityIdentifier.jmapSubmission: SubmissionCapability( + maxDelayedSend: UnsignedInt(0), + submissionExtensions: {} + ), CapabilityIdentifier.jmapWebSocket: WebSocketCapability( - true, Uri.parse('ws://domain.com/jmap/ws')), + supportsPush: true, + url: Uri.parse('ws://domain.com/jmap/ws') + ), CapabilityIdentifier.jmapMail: MailCapability( - UnsignedInt(10000000), - null, - UnsignedInt(200), - UnsignedInt(20000000), - {"receivedAt", "sentAt", "size", "from", "to", "subject"}, - true), + maxMailboxesPerEmail: UnsignedInt(10000000), + maxSizeAttachmentsPerEmail: UnsignedInt(20000000), + emailQuerySortOptions: {"receivedAt", "sentAt", "size", "from", "to", "subject"}, + mayCreateTopLevelMailbox: true + ), CapabilityIdentifier( Uri.parse('urn:apache:james:params:jmap:mail:quota')): DefaultCapability({}), @@ -110,17 +114,20 @@ void main() { test('when account capabilities is empty exception should throw', () { final session = Session( { - CapabilityIdentifier.jmapSubmission: - SubmissionCapability(UnsignedInt(0), {}), + CapabilityIdentifier.jmapSubmission: SubmissionCapability( + maxDelayedSend: UnsignedInt(0), + submissionExtensions: {} + ), CapabilityIdentifier.jmapMail: MailCapability( - UnsignedInt(10000000), - null, - UnsignedInt(200), - UnsignedInt(20000000), - {"receivedAt", "sentAt", "size", "from", "to", "subject"}, - true), - CapabilityIdentifier.jmapWebSocket: - WebSocketCapability(true, Uri.parse('ws://domain.com/jmap/ws')), + maxMailboxesPerEmail: UnsignedInt(10000000), + maxSizeAttachmentsPerEmail: UnsignedInt(20000000), + emailQuerySortOptions: {"receivedAt", "sentAt", "size", "from", "to", "subject"}, + mayCreateTopLevelMailbox: true + ), + CapabilityIdentifier.jmapWebSocket: WebSocketCapability( + supportsPush: true, + url: Uri.parse('ws://domain.com/jmap/ws') + ), CapabilityIdentifier( Uri.parse('urn:apache:james:params:jmap:mail:quota')): DefaultCapability({}), @@ -177,17 +184,20 @@ void main() { final invalidId = Id("29883977c13473ae7cb7678ef767cbffabfc8a44a6e463d971d23a65c1dc4af6"); final session = Session( { - CapabilityIdentifier.jmapSubmission: - SubmissionCapability(UnsignedInt(0), {}), + CapabilityIdentifier.jmapSubmission: SubmissionCapability( + maxDelayedSend: UnsignedInt(0), + submissionExtensions: {} + ), CapabilityIdentifier.jmapMail: MailCapability( - UnsignedInt(10000000), - null, - UnsignedInt(200), - UnsignedInt(20000000), - {"receivedAt", "sentAt", "size", "from", "to", "subject"}, - true), - CapabilityIdentifier.jmapWebSocket: - WebSocketCapability(true, Uri.parse('ws://domain.com/jmap/ws')), + maxMailboxesPerEmail: UnsignedInt(10000000), + maxSizeAttachmentsPerEmail: UnsignedInt(20000000), + emailQuerySortOptions: {"receivedAt", "sentAt", "size", "from", "to", "subject"}, + mayCreateTopLevelMailbox: true + ), + CapabilityIdentifier.jmapWebSocket: WebSocketCapability( + supportsPush: true, + url: Uri.parse('ws://domain.com/jmap/ws') + ), CapabilityIdentifier( Uri.parse('urn:apache:james:params:jmap:mail:quota')): DefaultCapability({}), @@ -201,17 +211,20 @@ void main() { AccountId(Id( '29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')): Account(AccountName('bob@domain.tld'), true, false, { - CapabilityIdentifier.jmapSubmission: - SubmissionCapability(UnsignedInt(0), {}), + CapabilityIdentifier.jmapSubmission: SubmissionCapability( + maxDelayedSend: UnsignedInt(0), + submissionExtensions: {} + ), CapabilityIdentifier.jmapWebSocket: WebSocketCapability( - true, Uri.parse('ws://domain.com/jmap/ws')), + supportsPush: true, + url: Uri.parse('ws://domain.com/jmap/ws') + ), CapabilityIdentifier.jmapMail: MailCapability( - UnsignedInt(10000000), - null, - UnsignedInt(200), - UnsignedInt(20000000), - {"receivedAt", "sentAt", "size", "from", "to", "subject"}, - true), + maxMailboxesPerEmail: UnsignedInt(10000000), + maxSizeAttachmentsPerEmail: UnsignedInt(20000000), + emailQuerySortOptions: {"receivedAt", "sentAt", "size", "from", "to", "subject"}, + mayCreateTopLevelMailbox: true + ), CapabilityIdentifier( Uri.parse('urn:apache:james:params:jmap:mail:quota')): DefaultCapability({}), @@ -263,17 +276,20 @@ void main() { test('when accounts empty should throw exception', () { final session = Session( { - CapabilityIdentifier.jmapSubmission: - SubmissionCapability(UnsignedInt(0), {}), + CapabilityIdentifier.jmapSubmission: SubmissionCapability( + maxDelayedSend: UnsignedInt(0), + submissionExtensions: {} + ), CapabilityIdentifier.jmapMail: MailCapability( - UnsignedInt(10000000), - null, - UnsignedInt(200), - UnsignedInt(20000000), - {"receivedAt", "sentAt", "size", "from", "to", "subject"}, - true), - CapabilityIdentifier.jmapWebSocket: - WebSocketCapability(true, Uri.parse('ws://domain.com/jmap/ws')), + maxMailboxesPerEmail: UnsignedInt(10000000), + maxSizeAttachmentsPerEmail: UnsignedInt(20000000), + emailQuerySortOptions: {"receivedAt", "sentAt", "size", "from", "to", "subject"}, + mayCreateTopLevelMailbox: true + ), + CapabilityIdentifier.jmapWebSocket: WebSocketCapability( + supportsPush: true, + url: Uri.parse('ws://domain.com/jmap/ws') + ), CapabilityIdentifier( Uri.parse('urn:apache:james:params:jmap:mail:quota')): DefaultCapability({}), @@ -325,27 +341,30 @@ void main() { test('when session have all required capabilities exception should not throw', () { final session = Session( { - CapabilityIdentifier.jmapSubmission: - SubmissionCapability(UnsignedInt(0), {}), + CapabilityIdentifier.jmapSubmission: SubmissionCapability( + maxDelayedSend: UnsignedInt(0), + submissionExtensions: {} + ), CapabilityIdentifier.jmapCore: CoreCapability( - UnsignedInt(20971520), - UnsignedInt(4), - UnsignedInt(10000000), - UnsignedInt(4), - UnsignedInt(16), - UnsignedInt(500), - UnsignedInt(500), - {CollationIdentifier("i;unicode-casemap")} + maxSizeUpload: UnsignedInt(20971520), + maxConcurrentUpload: UnsignedInt(4), + maxSizeRequest: UnsignedInt(10000000), + maxConcurrentRequests: UnsignedInt(4), + maxCallsInRequest: UnsignedInt(16), + maxObjectsInGet: UnsignedInt(500), + maxObjectsInSet: UnsignedInt(500), + collationAlgorithms: {CollationIdentifier("i;unicode-casemap")} ), CapabilityIdentifier.jmapMail: MailCapability( - UnsignedInt(10000000), - null, - UnsignedInt(200), - UnsignedInt(20000000), - {"receivedAt", "sentAt", "size", "from", "to", "subject"}, - true), - CapabilityIdentifier.jmapWebSocket: - WebSocketCapability(true, Uri.parse('ws://domain.com/jmap/ws')), + maxMailboxesPerEmail: UnsignedInt(10000000), + maxSizeAttachmentsPerEmail: UnsignedInt(20000000), + emailQuerySortOptions: {"receivedAt", "sentAt", "size", "from", "to", "subject"}, + mayCreateTopLevelMailbox: true + ), + CapabilityIdentifier.jmapWebSocket: WebSocketCapability( + supportsPush: true, + url: Uri.parse('ws://domain.com/jmap/ws') + ), CapabilityIdentifier( Uri.parse('urn:apache:james:params:jmap:mail:quota')): DefaultCapability({}), @@ -359,26 +378,26 @@ void main() { AccountId(Id( '29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6')): Account(AccountName('bob@domain.tld'), true, false, { - CapabilityIdentifier.jmapSubmission: - SubmissionCapability(UnsignedInt(0), {}), - CapabilityIdentifier.jmapWebSocket: WebSocketCapability( - true, Uri.parse('ws://domain.com/jmap/ws')), + CapabilityIdentifier.jmapSubmission: SubmissionCapability( + maxDelayedSend: UnsignedInt(0), + submissionExtensions: {} + ), CapabilityIdentifier.jmapCore: CoreCapability( - UnsignedInt(20971520), - UnsignedInt(4), - UnsignedInt(10000000), - UnsignedInt(4), - UnsignedInt(16), - UnsignedInt(500), - UnsignedInt(500), - {CollationIdentifier("i;unicode-casemap")}), + maxSizeUpload: UnsignedInt(20971520), + maxConcurrentUpload: UnsignedInt(4), + maxSizeRequest: UnsignedInt(10000000), + maxConcurrentRequests: UnsignedInt(4), + maxCallsInRequest: UnsignedInt(16), + maxObjectsInGet: UnsignedInt(500), + maxObjectsInSet: UnsignedInt(500), + collationAlgorithms: {CollationIdentifier("i;unicode-casemap")} + ), CapabilityIdentifier.jmapMail: MailCapability( - UnsignedInt(10000000), - null, - UnsignedInt(200), - UnsignedInt(20000000), - {"receivedAt", "sentAt", "size", "from", "to", "subject"}, - true), + maxMailboxesPerEmail: UnsignedInt(10000000), + maxSizeAttachmentsPerEmail: UnsignedInt(20000000), + emailQuerySortOptions: {"receivedAt", "sentAt", "size", "from", "to", "subject"}, + mayCreateTopLevelMailbox: true + ), CapabilityIdentifier( Uri.parse('urn:apache:james:params:jmap:mail:quota')): DefaultCapability({}), diff --git a/test/main/routes/route_utils_test.dart b/test/main/routes/route_utils_test.dart new file mode 100644 index 0000000000..b74042a20f --- /dev/null +++ b/test/main/routes/route_utils_test.dart @@ -0,0 +1,69 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:tmail_ui_user/main/routes/app_routes.dart'; +import 'package:tmail_ui_user/main/routes/route_utils.dart'; + +void main() { + group('parseMapMailtoFromUri test', () { + test('should parse a valid mailto URI', () { + const mailtoUri = 'mailto:test@example.com?subject=Hello'; + final result = RouteUtils.parseMapMailtoFromUri(mailtoUri); + + expect(result[RouteUtils.paramRouteName], equals(AppRoutes.mailtoURL)); + expect(result[RouteUtils.paramMailtoAddress], equals('test@example.com')); + expect(result[RouteUtils.paramSubject], equals('Hello')); + }); + + test('should parse a valid mailto URI encoded', () { + const mailtoUri = 'mailto:test%40example.com%3Fsubject=Hello'; + final result = RouteUtils.parseMapMailtoFromUri(mailtoUri); + + expect(result[RouteUtils.paramRouteName], equals(AppRoutes.mailtoURL)); + expect(result[RouteUtils.paramMailtoAddress], equals('test@example.com')); + expect(result[RouteUtils.paramSubject], equals('Hello')); + }); + + test('should handle a mailto URI without subject', () { + const mailtoUri = 'mailto:test@example.com'; + final result = RouteUtils.parseMapMailtoFromUri(mailtoUri); + + expect(result[RouteUtils.paramRouteName], equals(AppRoutes.mailtoURL)); + expect(result[RouteUtils.paramMailtoAddress], equals('test@example.com')); + expect(result[RouteUtils.paramSubject], isNull); + }); + + test('should handle a mailto URI without subject encoded', () { + const mailtoUri = 'mailto:test%40example.com'; + final result = RouteUtils.parseMapMailtoFromUri(mailtoUri); + + expect(result[RouteUtils.paramRouteName], equals(AppRoutes.mailtoURL)); + expect(result[RouteUtils.paramMailtoAddress], equals('test@example.com')); + expect(result[RouteUtils.paramSubject], isNull); + }); + + test('should handle a non-mailto URI', () { + const nonMailtoUri = 'test@example.com'; + final result = RouteUtils.parseMapMailtoFromUri(nonMailtoUri); + + expect(result[RouteUtils.paramRouteName], equals(AppRoutes.mailtoURL)); + expect(result[RouteUtils.paramMailtoAddress], equals(nonMailtoUri)); + expect(result[RouteUtils.paramSubject], isNull); + }); + + test('should handle a non-mailto URI encoded', () { + const nonMailtoUri = 'test%40example.com'; + final result = RouteUtils.parseMapMailtoFromUri(nonMailtoUri); + + expect(result[RouteUtils.paramRouteName], equals(AppRoutes.mailtoURL)); + expect(result[RouteUtils.paramMailtoAddress], equals('test@example.com')); + expect(result[RouteUtils.paramSubject], isNull); + }); + + test('should handle null input', () { + final result = RouteUtils.parseMapMailtoFromUri(null); + + expect(result[RouteUtils.paramRouteName], equals(AppRoutes.mailtoURL)); + expect(result[RouteUtils.paramMailtoAddress], isNull); + expect(result[RouteUtils.paramSubject], isNull); + }); + }); +} \ No newline at end of file diff --git a/test/main/utils/app_utils_test.dart b/test/main/utils/app_utils_test.dart new file mode 100644 index 0000000000..93f71cfbfc --- /dev/null +++ b/test/main/utils/app_utils_test.dart @@ -0,0 +1,37 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:jmap_dart_client/jmap/mail/email/email_address.dart'; +import 'package:model/model.dart'; +import 'package:tmail_ui_user/main/utils/app_utils.dart'; + +void main() { + group('Validation email address test', () { + + final emailsValid = [ + EmailAddress("userName", "alice@localhost"), + EmailAddress("userName", "bob@localhost"), + EmailAddress("userName", "123@localhost"), + EmailAddress("userName", "alic.2312e@localhost"), + EmailAddress("userName", "alice_linagora@localhost") + ]; + + final emailsInvalid = [ + EmailAddress("userName", "localhost"), + EmailAddress("userName", "bob@local"), + EmailAddress("userName", "123@domain"), + EmailAddress("userName", "alic.2312e@"), + EmailAddress("userName", "a,.2312lic.2312e@") + ]; + + test('Valid localhost email addresses', () { + for (final email in emailsValid) { + expect(AppUtils.isEmailLocalhost(email.emailAddress), equals(true)); + } + }); + + test('Invalid localhost email addresses', () { + for (final email in emailsInvalid) { + expect(AppUtils.isEmailLocalhost(email.emailAddress), equals(false)); + } + }); + }); +} \ No newline at end of file diff --git a/web/firebase-messaging-sw.js b/web/firebase-messaging-sw.js index cf5dd038dd..f28471b413 100644 --- a/web/firebase-messaging-sw.js +++ b/web/firebase-messaging-sw.js @@ -1,6 +1,6 @@ importScripts("https://www.gstatic.com/firebasejs/9.10.0/firebase-app-compat.js"); importScripts("https://www.gstatic.com/firebasejs/9.10.0/firebase-messaging-compat.js"); - +// Initialize the Firebase app in the service worker by passing in the messagingSenderId. firebase.initializeApp({ apiKey: "...", authDomain: "...", @@ -10,10 +10,5 @@ firebase.initializeApp({ messagingSenderId: "...", appId: "...", }); -const messaging = firebase.messaging(); - -messaging.setBackgroundMessageHandler(function(payload) { - console.log('[firebase-messaging-sw.js] Received background message ', payload); - self.registration.hideNotification(); - return null; -}); \ No newline at end of file +// Retrieve an instance of Firebase Messaging so that it can handle background messages. +const messaging = firebase.messaging(); \ No newline at end of file diff --git a/web/index.html b/web/index.html index 88e38834d1..3c60c158c5 100644 --- a/web/index.html +++ b/web/index.html @@ -31,8 +31,30 @@ Team Mail + + + + + + + + + + + + +
+
+ + + + + +
+
+
@@ -56,7 +78,12 @@ // Wait for registration to finish before dropping the