diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 65ced8e53c..1ddce5067a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -6,7 +6,7 @@ on: name: Build env: - FLUTTER_VERSION: 3.22.2 + FLUTTER_VERSION: 3.24.0 XCODE_VERSION: ^15.0.1 jobs: diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml index a05f24d3f1..60d8b36c78 100644 --- a/.github/workflows/gh-pages.yaml +++ b/.github/workflows/gh-pages.yaml @@ -2,7 +2,7 @@ on: pull_request: env: - FLUTTER_VERSION: 3.22.2 + FLUTTER_VERSION: 3.24.0 LIBOLM_VERSION: 3.2.16 name: Deploying on GitHub Pages diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9900cf2e99..6f78ddbfbf 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,7 +4,7 @@ on: - "v*.*.*" env: - FLUTTER_VERSION: 3.22.2 + FLUTTER_VERSION: 3.24.0 XCODE_VERSION: ^15.0.1 name: Release diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 69d251ff18..685041ed13 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -4,7 +4,7 @@ on: name: Tests env: - FLUTTER_VERSION: 3.22.2 + FLUTTER_VERSION: 3.24.0 jobs: code_analyze: diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2dcffc6a5a..f048d25f30 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - FLUTTER_VERSION: 3.22.2 + FLUTTER_VERSION: 3.24.0 image: name: cirrusci/flutter:${FLUTTER_VERSION} diff --git a/CHANGELOG.md b/CHANGELOG.md index c1e5c21ca8..8015581ce2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,33 @@ +## [2.6.0+2330] - 2024-07-18 + +### Fixed + +- #1879 Fix online status is not updated correctly +- #1892 Handle error recovery key lost +- #1898 Fix memory leak in file picker +- #1897 Fix profile image is not updated in multiple account +- #1903 Check dialog status before sending file +- #1911 Fix can't open chat when search exact Matrix ID +- #1921 Fix iOS is forced to log out many times +- #1910 Remove x when searchbar is empty +- #1938 Fix wrong responsive when size of screen is small +- #1930 Improve search exact Matrix ID inside Contact tab +- #1948 Fix jump exactly to message in the notification on Mobile +- #1946 Fix 500,404 error in POST request when login + +### Added + +- #1940 Upload feature +- #1890 Renamed artifact to describe the OS +- #1880 Standarlize Appbar and Appgrid popup +- #1889 Change style loading dialog +- #1894 Change message info dialog close button +- #1905 Update online status based on design +- #1937 Improve style for backup dialog +- #1951 Integration dynamic link on android mobile +- #1944 Update quick actions +- #1956 Create permission dialogs for contacts and media + ## [2.5.8+2330] - 2024-05-25 - #1781 Upgrade to Flutter SDK 3.22.0 @@ -2353,6 +2383,7 @@ interesting devices. If you have one, I would very like to see some screenshots This CHANGELOG.md was generated with [**Changelog for Dart**](https://pub.dartlang.org/packages/changelog) +[2.6.0+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.6.0 [2.5.2+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.5.2 [2.4.20+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.4.20 [2.4.19+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.4.19 diff --git a/Dockerfile b/Dockerfile index 8843fe8c2f..705320acd4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Specify versions -ARG FLUTTER_VERSION=3.22.2 +ARG FLUTTER_VERSION=3.24.0 ARG OLM_VERSION=3.2.16 ARG NIX_VERSION=2.22.1 diff --git a/android/app/build.gradle b/android/app/build.gradle index cee5c1f7b6..191d884db1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -46,7 +46,7 @@ android { defaultConfig { applicationId "app.twake.android.chat" minSdkVersion 23 - targetSdkVersion 33 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5dbba0bcfc..58016494b2 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -61,26 +61,12 @@ - - - - - - - - - - - - diff --git a/android/fastlane/Fastfile b/android/fastlane/Fastfile index 4484f8e15d..6d9f2d881c 100644 --- a/android/fastlane/Fastfile +++ b/android/fastlane/Fastfile @@ -37,7 +37,7 @@ platform :android do track = "internal" upload_to_play_store(track: track, aab: '../build/app/outputs/bundle/release/app-release.aab', - release_status: "draft") + release_status: "completed") end lane :deploy_candidate do diff --git a/assets/images/ic_encrypted.svg b/assets/images/ic_encrypted.svg index f082a53915..4a461b2b26 100644 --- a/assets/images/ic_encrypted.svg +++ b/assets/images/ic_encrypted.svg @@ -1,7 +1,3 @@ - - - - - - + + diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index cea487f0fe..c2ec72dcc2 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -899,7 +899,7 @@ "type": "text", "placeholders": {} }, - "friday": "Friday", + "friday": "Fri", "@friday": { "type": "text", "placeholders": {} @@ -1180,8 +1180,7 @@ "type": "text", "placeholders": {} }, - "loading": "Loading...", - "@loading": {}, + "loadingStatus": "Loading status...", "loadMore": "Load more…", "@loadMore": { "type": "text", @@ -1248,7 +1247,7 @@ "type": "text", "placeholders": {} }, - "monday": "Monday", + "monday": "Mon", "@monday": { "type": "text", "placeholders": {} @@ -1693,7 +1692,7 @@ "type": "text", "placeholders": {} }, - "saturday": "Saturday", + "saturday": "Sat", "@saturday": { "type": "text", "placeholders": {} @@ -1953,7 +1952,7 @@ "type": "text", "placeholders": {} }, - "sunday": "Sunday", + "sunday": "Sun", "@sunday": { "type": "text", "placeholders": {} @@ -1983,7 +1982,7 @@ "type": "text", "placeholders": {} }, - "thursday": "Thursday", + "thursday": "Thu", "@thursday": { "type": "text", "placeholders": {} @@ -2024,7 +2023,7 @@ "type": "text", "placeholders": {} }, - "tuesday": "Tuesday", + "tuesday": "Tue", "@tuesday": { "type": "text", "placeholders": {} @@ -2201,7 +2200,7 @@ "type": "text", "placeholders": {} }, - "wednesday": "Wednesday", + "wednesday": "Wed", "@wednesday": { "type": "text", "placeholders": {} @@ -2582,7 +2581,7 @@ }, "noMessageHereYet": "No message here yet...", "@noMessageHereYet": {}, - "sendMessageGuide": "Send a message or tap on the greeting bellow.", + "sendMessageGuide": "Send a message or tap on the greeting below.", "@sendMessageGuide": {}, "youCreatedGroupChat": "You created a Group chat", "@youCreatedGroupChat": {}, @@ -2734,7 +2733,7 @@ "@chatsAndContacts": {}, "externalContactTitle": "Invite new users", "@externalContactTitle": {}, - "externalContactMessage": "Some of the users you want to add are not in your contacs. Do you want to invite them?", + "externalContactMessage": "Some of the users you want to add are not in your contacts. Do you want to invite them?", "@externalContactMessage": {}, "clear": "Clear", "@clear": {}, @@ -3102,6 +3101,10 @@ } } }, + "explainPermissionToAccessContacts": "Twake Chat DOES NOT collect your contacts. Twake Chat sends only contact hashes to the Twake Chat servers to understand who from your friends already joined Twake Chat, enabling connection with them. Your contacts ARE NOT synchronized with our server.", + "explainPermissionToAccessMedias": "Twake Chat does not synchronize data between your device and our servers. We only store media that you have sent to the chat room. All media files sent to chat are encrypted and stored securely. Go to Settings > Permissions and activate the Storage: Photos and Videos permission. You can also deny access to your media library at any time.", + "explainPermissionToAccessPhotos": "Twake Chat does not synchronize data between your device and our servers. We only store media that you have sent to the chat room. All media files sent to chat are encrypted and stored securely. Go to Settings > Permissions and activate the Storage: Photos permission. You can also deny access to your media library at any time.", + "explainPermissionToAccessVideos": "Twake Chat does not synchronize data between your device and our servers. We only store media that you have sent to the chat room. All media files sent to chat are encrypted and stored securely. Go to Settings > Permissions and activate the Storage: Videos permission. You can also deny access to your media library at any time.", "downloading": "Downloading", "@downloading": {}, "settingUpYourTwake": "Setting up your Twake\nIt could take a while", @@ -3163,7 +3166,24 @@ "byContinuingYourAgreeingToOur": "By continuing, you're agreeing to our", "@byContinuingYourAgreeingToOur": {}, "youDontHaveAnyContactsYet": "You dont have any contacts yet.", - "@youDontHaveAnyContactsYet": {}, + "loading": "Loading...", "errorDialogTitle": "Oops, something went wrong", - "@errorDialogTitle": {} + "shootingTips": "Tap to take photo.", + "shootingWithRecordingTips": "Tap to take photo. Long press to record video.", + "shootingOnlyRecordingTips": "Long press to record video.", + "shootingTapRecordingTips": "Tap to record video.", + "loadFailed": "Load failed", + "saving": "Saving...", + "sActionManuallyFocusHint": "Manually focus", + "sActionPreviewHint": "Preview", + "sActionRecordHint": "Record", + "sActionShootHint": "Take picture", + "sActionShootingButtonTooltip": "Shooting button", + "sActionStopRecordingHint": "Stop recording", + "sCameraLensDirectionLabel": "Camera lens direction: {value}", + "sCameraPreviewLabel": "Camera preview: {value}", + "sFlashModeLabel": "Flash mode: {mode}", + "sSwitchCameraLensDirectionLabel": "Switch to the {value} camera", + "photo": "Photo", + "video": "Video" } diff --git a/assets/l10n/intl_fr.arb b/assets/l10n/intl_fr.arb index b10730d60a..61bcf1b3fd 100644 --- a/assets/l10n/intl_fr.arb +++ b/assets/l10n/intl_fr.arb @@ -89,7 +89,7 @@ "type": "text", "placeholders": {} }, - "areGuestsAllowedToJoin": "Les invités peuvent-i·e·ls rejoindre", + "areGuestsAllowedToJoin": "Les invités peuvent-ils/elles rejoindre", "@areGuestsAllowedToJoin": { "type": "text", "placeholders": {} @@ -358,7 +358,7 @@ "type": "text", "placeholders": {} }, - "chooseAUsername": "Choisissez un nom d'utilisateur·ice", + "chooseAUsername": "Choisissez un nom d'utilisateur/trice", "@chooseAUsername": { "type": "text", "placeholders": {} @@ -809,11 +809,6 @@ "type": "text", "placeholders": {} }, - "friday": "Vendredi", - "@friday": { - "type": "text", - "placeholders": {} - }, "fromJoining": "À partir de l'entrée dans le salon", "@fromJoining": { "type": "text", @@ -1143,11 +1138,6 @@ "type": "text", "placeholders": {} }, - "monday": "Lundi", - "@monday": { - "type": "text", - "placeholders": {} - }, "muteChat": "Mettre la discussion en sourdine", "@muteChat": { "type": "text", @@ -1541,11 +1531,6 @@ "type": "text", "placeholders": {} }, - "saturday": "Samedi", - "@saturday": { - "type": "text", - "placeholders": {} - }, "saveFile": "Enregistrer le fichier", "@saveFile": { "type": "text", @@ -1786,11 +1771,6 @@ "type": "text", "placeholders": {} }, - "sunday": "Dimanche", - "@sunday": { - "type": "text", - "placeholders": {} - }, "synchronizingPleaseWait": "Synchronisation... Veuillez patienter.", "@synchronizingPleaseWait": { "type": "text", @@ -1816,11 +1796,6 @@ "type": "text", "placeholders": {} }, - "thursday": "Jeudi", - "@thursday": { - "type": "text", - "placeholders": {} - }, "title": "FluffyChat", "@title": { "description": "Title for the application", @@ -1857,11 +1832,6 @@ "type": "text", "placeholders": {} }, - "tuesday": "Mardi", - "@tuesday": { - "type": "text", - "placeholders": {} - }, "unavailable": "Indisponible", "@unavailable": { "type": "text", @@ -2032,11 +2002,6 @@ "type": "text", "placeholders": {} }, - "wednesday": "Mercredi", - "@wednesday": { - "type": "text", - "placeholders": {} - }, "weSentYouAnEmail": "Nous vous avons envoyé un message", "@weSentYouAnEmail": { "type": "text", @@ -2715,6 +2680,10 @@ "count": {} } }, + "explainPermissionToAccessContacts": "", + "explainPermissionToAccessMedias": "", + "explainPermissionToAccessPhotos": "", + "explainPermissionToAccessVideos": "", "recentChat": "DISCUSSION RÉCENTE", "@recentChat": {}, "muteThisMessage": "Couper le son de ce salon", @@ -3086,8 +3055,7 @@ "@viewProfile": {}, "profileInfo": "Informations du profil", "@profileInfo": {}, - "loading": "Chargement en cours...", - "@loading": {}, + "loadingStatus": "Chargement en cours...", "onlineDayAgo": "en ligne il y a {day} jours", "@onlineDayAgo": { "placeholders": { @@ -3187,5 +3155,22 @@ "removeUser": "Supprimer l'utilisateur", "@removeUser": {}, "switchAccounts": "Changer de compte", - "@switchAccounts": {} + "@switchAccounts": {}, + "shootingTips": "Appuyez pour prendre une photo.", + "shootingWithRecordingTips": "Appuyez pour prendre une photo. Appuyez longuement pour enregistrer une vidéo.", + "shootingOnlyRecordingTips": "Appuyez longuement pour enregistrer une vidéo.", + "shootingTapRecordingTips": "Appuyez pour enregistrer une vidéo.", + "loadFailed": "Échec du chargement", + "loading": "Chargement...", + "saving": "Enregistrement...", + "sActionManuallyFocusHint": "mettre au point manuellement", + "sActionPreviewHint": "aperçu", + "sActionRecordHint": "enregistrer", + "sActionShootHint": "prendre une photo", + "sActionShootingButtonTooltip": "bouton de prise de vue", + "sActionStopRecordingHint": "arrêter l'enregistrement", + "sCameraLensDirectionLabel": "Direction de la lentille de la caméra : {value}", + "sCameraPreviewLabel": "Aperçu de la caméra : {value}", + "sFlashModeLabel": "Mode flash : {mode}", + "sSwitchCameraLensDirectionLabel": "Passer à la caméra {value}" } diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index 7c071887ac..adb94bceaf 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -800,11 +800,6 @@ "type": "text", "placeholders": {} }, - "friday": "Пятница", - "@friday": { - "type": "text", - "placeholders": {} - }, "fromJoining": "С момента присоединения", "@fromJoining": { "type": "text", @@ -1134,11 +1129,6 @@ "type": "text", "placeholders": {} }, - "monday": "Понедельник", - "@monday": { - "type": "text", - "placeholders": {} - }, "muteChat": "Отключить уведомления", "@muteChat": { "type": "text", @@ -1527,11 +1517,6 @@ "type": "text", "placeholders": {} }, - "saturday": "Суббота", - "@saturday": { - "type": "text", - "placeholders": {} - }, "saveFile": "Сохранить файл", "@saveFile": { "type": "text", @@ -1770,11 +1755,6 @@ "type": "text", "placeholders": {} }, - "sunday": "Воскресенье", - "@sunday": { - "type": "text", - "placeholders": {} - }, "synchronizingPleaseWait": "Синхронизация… Пожалуйста, подождите.", "@synchronizingPleaseWait": { "type": "text", @@ -1800,11 +1780,6 @@ "type": "text", "placeholders": {} }, - "thursday": "Четверг", - "@thursday": { - "type": "text", - "placeholders": {} - }, "title": "FluffyChat", "@title": { "description": "Title for the application", @@ -1841,11 +1816,6 @@ "type": "text", "placeholders": {} }, - "tuesday": "Вторник", - "@tuesday": { - "type": "text", - "placeholders": {} - }, "unavailable": "Недоступен", "@unavailable": { "type": "text", @@ -2016,11 +1986,6 @@ "type": "text", "placeholders": {} }, - "wednesday": "Среда", - "@wednesday": { - "type": "text", - "placeholders": {} - }, "weSentYouAnEmail": "Мы отправили вам электронное письмо", "@weSentYouAnEmail": { "type": "text", @@ -3191,4 +3156,21 @@ "@privacyPolicy": {}, "byContinuingYourAgreeingToOur": "Продолжая, вы принимаете", "@byContinuingYourAgreeingToOur": {} + "shootingTips": "Нажмите, чтобы сделать фото.", + "shootingWithRecordingTips": "Нажмите, чтобы сделать фото. Удерживайте для записи видео.", + "shootingOnlyRecordingTips": "Удерживайте для записи видео.", + "shootingTapRecordingTips": "Нажмите, чтобы записать видео.", + "loadFailed": "Не удалось загрузить", + "loading": "Загрузка...", + "saving": "Сохранение...", + "sActionManuallyFocusHint": "ручная фокусировка", + "sActionPreviewHint": "предпросмотр", + "sActionRecordHint": "запись", + "sActionShootHint": "сделать фото", + "sActionShootingButtonTooltip": "кнопка съемки", + "sActionStopRecordingHint": "остановить запись", + "sCameraLensDirectionLabel": "Направление объектива камеры: {value}", + "sCameraPreviewLabel": "Предпросмотр камеры: {value}", + "sFlashModeLabel": "Режим вспышки: {mode}", + "sSwitchCameraLensDirectionLabel": "Переключиться на камеру {value}" } diff --git a/docs/adr/0024-cancel-upload-file-with-caption.md b/docs/adr/0024-cancel-upload-file-with-caption.md new file mode 100644 index 0000000000..9110ce3a77 --- /dev/null +++ b/docs/adr/0024-cancel-upload-file-with-caption.md @@ -0,0 +1,40 @@ +# 24. Cancel upload file with caption + +Date: 2024-08-19 + +## Status + +Accepted + +- Issue: [#1972](https://github.com/linagora/twake-on-matrix/issues/1972) + +## Context + +- The caption is sent anyway, there's also a weird upload preview that disappears after you reload the page. + +## Decision +1. **Update logic for Cancelling Upload files with captions** + - If the user uploads files with captions (on Web and mobile), update the logic for this case: + - In the last element of the file upload list, add information about the caption. + - When the user cancels a file upload, check if the file has associated caption information, and remove both the file and the caption. + - If the file doesn’t have caption information, remove only the file. + - Example: + - User upload 3 files: [ file1, file2, file3]. + - User add a caption for upload files. => [ file1, file2, file3 (caption)]. + - And add an information of caption to last file. `file3` has an information. + - If user cancel upload cancel randomly a file. + - Check if the caption information is not null. + - If the caption information is not null => remove both the file and the caption. + - If the caption information is null => remove only the file. + - If the user cancels the upload of `file1`, only `file1` is removed. + - If the user cancels the upload of `file2`, only `file2` is removed.. + - If the user cancels the upload of `file3`, both `file3` and the caption are removed. + +2. **Update UI/UX for Cancelling Upload** + - Display a cancel upload icon during file upload and while receiving upload progress. + - If upload progress cannot be received, only display a loading icon and disable the cancel upload option. + +## Consequences + +- Cancel upload of media, the caption isn't sent either. +- There's no weird preview if I canceled upload \ No newline at end of file diff --git a/docs/adr/0024-upgrade-flutter-3.24.md b/docs/adr/0024-upgrade-flutter-3.24.md new file mode 100644 index 0000000000..9174d5eeac --- /dev/null +++ b/docs/adr/0024-upgrade-flutter-3.24.md @@ -0,0 +1,27 @@ +# 23. Flutter 3.24.0 Upgrade + +**Date:** 2024-08-26 + +## Status + +**Accepted** + +## Context + +To enable features like text pasting without requiring user permission on iOS, we need to upgrade to Flutter 3.24.0.[See detail](https://github.com/flutter/flutter/issues/103163#issuecomment-2320611190) + +## Decision + +During the upgrade to Flutter 3.24.0, some dependencies must also be updated to ensure successful release builds. Specifically, the `compileSdkVersion` needs to be updated to at least 31. Below is the list of packages that require a minimum `compileSdkVersion` of 31 or higher for successful compilation: + +- **`callkeep`**: Upgrade `compileSdkVersion` from 28 to 31. [See PR #190](https://github.com/flutter-webrtc/callkeep/pull/190) +- **`receive_sharing_intent`**: Switch to the version maintained by Linagora, which has `compileSdkVersion` set to 33. [See Linagora Repository](https://github.com/linagora/receive_sharing_intent) +- **`flutter_contacts`**: Upgrade `compileSdkVersion` from 31 to 33. [See PR #11](https://github.com/linagora/flutter_contacts/pull/11) +- **`fcm_shared_isolate`**: [See Merge Request](https://gitlab.com/famedly/company/frontend/libraries/fcm_shared_isolate/-/merge_requests/10) +- **`native_image`**: [See Merge Request](https://gitlab.com/famedly/company/frontend/libraries/native_imaging/-/merge_requests/22) +- **`flutter_app_badger`**: [See PR #92](https://github.com/g123k/flutter_app_badger/pull/92) +- **`image_gallery_saver`**: [See GitHub Repository](https://github.com/FlutterStudioIst/image_gallery_saver.git) + +Additionally, update `@UIApplicationMain` to `@main` in the codebase. For more information, refer to [Swift Evolution Proposal #0383](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0383-deprecate-uiapplicationmain-and-nsapplicationmain.md). + +The build debug version of window platform has not been success, due to the error tracking here: https://github.com/firebase/flutterfire/issues/12051. diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 6fe9d3c340..98384d5d84 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -3,7 +3,7 @@ import Flutter let apnTokenKey = "apnToken" -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { var twakeApnChannel: FlutterMethodChannel? var initialNotiInfo: Any? diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 09f84c35b5..1d33ddf0e2 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -6,7 +6,9 @@ import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:matrix/matrix.dart'; +import 'package:video_thumbnail/video_thumbnail.dart'; abstract class AppConfig { static ResponsiveUtils responsive = getIt.get(); @@ -115,6 +117,8 @@ abstract class AppConfig { static const String iOSKeychainSharingAccount = 'app.twake.ios.chat.sessions'; static const int maxFilesSendPerDialog = 6; static const bool supportMultipleAccountsInTheSameHomeserver = false; + static const imageCompressFormmat = CompressFormat.jpeg; + static const videoThumbnailFormat = ImageFormat.JPEG; static String? issueId; diff --git a/lib/config/go_routes/go_router.dart b/lib/config/go_routes/go_router.dart index 808467d4c1..b38bdec6e1 100644 --- a/lib/config/go_routes/go_router.dart +++ b/lib/config/go_routes/go_router.dart @@ -17,7 +17,6 @@ import 'package:fluffychat/pages/login/on_auth_redirect.dart'; import 'package:fluffychat/pages/new_group/new_group_chat_info.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_app_language/settings_app_language.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile.dart'; -import 'package:fluffychat/pages/share/share.dart'; import 'package:fluffychat/pages/story/story_page.dart'; import 'package:fluffychat/pages/twake_welcome/twake_welcome.dart'; import 'package:fluffychat/presentation/model/chat/chat_router_input_argument.dart'; @@ -519,14 +518,6 @@ abstract class AppRoutes { ), ], ), - GoRoute( - path: '/share', - pageBuilder: (context, state) => defaultPageBuilder( - context, - const Share(), - ), - redirect: loggedOutRedirect, - ), ], ), ]; diff --git a/lib/config/themes.dart b/lib/config/themes.dart index d474774b73..22cd1a9668 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -1,3 +1,5 @@ +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; @@ -19,6 +21,8 @@ abstract class TwakeThemes { static bool getDisplayNavigationRail(BuildContext context) => !(GoRouterState.of(context).path?.startsWith('/settings') == true); + static ResponsiveUtils responsive = getIt.get(); + static var fallbackTextTheme = TextTheme( bodyLarge: GoogleFonts.inter( fontWeight: FontWeight.w500, @@ -64,6 +68,7 @@ abstract class TwakeThemes { ), titleLarge: GoogleFonts.inter( fontWeight: FontWeight.w600, + letterSpacing: -0.15, ), titleMedium: GoogleFonts.inter( fontWeight: FontWeight.w500, @@ -157,6 +162,7 @@ abstract class TwakeThemes { ), ), ), + highlightColor: LinagoraRefColors.material().tertiary[80], colorScheme: ColorScheme.fromSeed( seedColor: seed ?? AppConfig.colorSchemeSeed, brightness: brightness, @@ -314,6 +320,25 @@ abstract class TwakeThemes { ), navigationBarTheme: NavigationBarThemeData( height: 64, + labelTextStyle: WidgetStateProperty.resolveWith( + (states) { + if (states.contains(WidgetState.selected)) { + return fallbackTextTheme.labelSmall?.copyWith( + fontSize: 11, + color: LinagoraSysColors.material().primary, + ); + } + return responsive.isDesktop(context) + ? fallbackTextTheme.labelSmall?.copyWith( + fontSize: 11, + color: LinagoraRefColors.material().neutral[10], + ) + : fallbackTextTheme.labelSmall?.copyWith( + fontSize: 11, + color: LinagoraSysColors.material().tertiary, + ); + }, + ), backgroundColor: brightness == Brightness.light ? LinagoraSysColors.material().surface : LinagoraSysColors.material().surfaceDark, @@ -321,10 +346,15 @@ abstract class TwakeThemes { ? Colors.black.withOpacity(0.15) : Colors.white.withOpacity(0.15), elevation: 4.0, + overlayColor: WidgetStateColor.resolveWith( + (states) { + return Colors.transparent; + }, + ), ), navigationRailTheme: NavigationRailThemeData( indicatorColor: brightness == Brightness.light - ? LinagoraSysColors.material().secondaryContainer + ? LinagoraSysColors.material().inversePrimary : LinagoraSysColors.material().secondaryContainerDark, ), bottomSheetTheme: BottomSheetThemeData( @@ -335,6 +365,19 @@ abstract class TwakeThemes { ? LinagoraSysColors.material().background : LinagoraSysColors.material().backgroundDark, ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: LinagoraSysColors.material().surface, + selectedLabelStyle: fallbackTextTheme.labelSmall?.copyWith( + fontSize: 11, + color: LinagoraSysColors.material().primary, + ), + unselectedLabelStyle: fallbackTextTheme.labelSmall?.copyWith( + fontSize: 11, + color: LinagoraSysColors.material().tertiary, + ), + selectedItemColor: LinagoraSysColors.material().primary, + unselectedItemColor: LinagoraSysColors.material().tertiary, + ), ); } diff --git a/lib/domain/contact_manager/contacts_manager.dart b/lib/domain/contact_manager/contacts_manager.dart index 81a86b2fdd..1cac10b553 100644 --- a/lib/domain/contact_manager/contacts_manager.dart +++ b/lib/domain/contact_manager/contacts_manager.dart @@ -17,6 +17,8 @@ class ContactsManager { bool _doNotShowWarningContactsBannerAgain = false; + bool _doNotShowWarningContactsDialogAgain = false; + final ValueNotifierCustom> _contactsNotifier = ValueNotifierCustom(const Right(ContactsInitial())); @@ -41,10 +43,17 @@ class ContactsManager { bool get isDoNotShowWarningContactsBannerAgain => _doNotShowWarningContactsBannerAgain; + bool get isDoNotShowWarningContactsDialogAgain => + _doNotShowWarningContactsDialogAgain; + set updateNotShowWarningContactsBannerAgain(bool value) { _doNotShowWarningContactsBannerAgain = value; } + set updateNotShowWarningContactsDialogAgain(bool value) { + _doNotShowWarningContactsDialogAgain = value; + } + Future reSyncContacts() async { _contactsNotifier.value = const Right(ContactsInitial()); _phonebookContactsNotifier.value = @@ -92,4 +101,7 @@ class ContactsManager { }, ); } + + void refreshPhonebookContacts() => + _fetchPhonebookContacts(isAvailableSupportPhonebookContacts: true); } diff --git a/lib/domain/model/room/room_list_extension.dart b/lib/domain/model/room/room_list_extension.dart index db1d5134b7..ce127227ca 100644 --- a/lib/domain/model/room/room_list_extension.dart +++ b/lib/domain/model/room/room_list_extension.dart @@ -3,6 +3,22 @@ import 'package:fluffychat/domain/model/search/recent_chat_model.dart'; import 'package:matrix/matrix.dart'; extension RoomListExtension on List { + bool _matchedMatrixId(RecentChatSearchModel model, String keyword) { + return model.directChatMatrixID + ?.toLowerCase() + .contains(keyword.toLowerCase()) ?? + false; + } + + bool _matchedName(RecentChatSearchModel model, String keyword) { + return model.displayName?.toLowerCase().contains(keyword.toLowerCase()) ?? + false; + } + + bool _matchedNameOrMatrixId(RecentChatSearchModel model, String keyword) { + return _matchedName(model, keyword) || _matchedMatrixId(model, keyword); + } + List searchRecentChat({ required MatrixLocalizations matrixLocalizations, required String keyword, @@ -13,11 +29,7 @@ extension RoomListExtension on List { ) .map((room) => room.toRecentChatSearchModel(matrixLocalizations)) .where( - (model) => - model.displayName != null && - model.displayName!.toLowerCase().contains( - keyword.toLowerCase(), - ), + (model) => _matchedNameOrMatrixId(model, keyword), ) .take(limit ?? length) .toList(); diff --git a/lib/domain/usecase/send_images_interactor.dart b/lib/domain/usecase/send_images_interactor.dart deleted file mode 100644 index 14a043e779..0000000000 --- a/lib/domain/usecase/send_images_interactor.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:fluffychat/presentation/extensions/send_file_extension.dart'; -import 'package:fluffychat/presentation/model/file/file_asset_entity.dart'; -import 'package:matrix/matrix.dart'; - -class SendMediaInteractor { - Future execute({ - required Room room, - required List entities, - String? caption, - }) async { - try { - final txIdMapToFileInfo = await room.sendPlaceholdersForImagePickerFiles( - entities: entities, - ); - - String? messageID; - Map? msgEventContent; - if (caption != null && caption.isNotEmpty) { - messageID = room.client.generateUniqueTransactionId(); - msgEventContent = room.getEventContentFromMsgText(message: caption); - await room.sendFakeMessage( - content: msgEventContent, - messageId: messageID, - ); - } - - for (final txId in txIdMapToFileInfo.keys) { - final fakeSendingFileInfo = txIdMapToFileInfo[txId]; - if (fakeSendingFileInfo == null) { - continue; - } - - await room.sendFileEventMobile( - fakeSendingFileInfo.fileInfo, - msgType: fakeSendingFileInfo.messageType, - fakeImageEvent: fakeSendingFileInfo.fakeImageEvent, - shrinkImageMaxDimension: 1600, - txid: txId, - ); - } - if (messageID != null && msgEventContent != null) { - await room.sendMessageContent( - EventTypes.Message, - msgEventContent, - txid: messageID, - ); - } - } catch (error) { - Logs().d("SendImagesInteractor: execute(): $error"); - } - } -} diff --git a/lib/pages/bootstrap/bootstrap_dialog.dart b/lib/pages/bootstrap/bootstrap_dialog.dart index 734147a0ba..143897e1f4 100644 --- a/lib/pages/bootstrap/bootstrap_dialog.dart +++ b/lib/pages/bootstrap/bootstrap_dialog.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/pages/bootstrap/tom_bootstrap_dialog.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/adaptive_flat_button.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -136,6 +137,7 @@ class BootstrapDialogState extends State { maxLines: 4, readOnly: true, style: GoogleFonts.robotoMono(), + contextMenuBuilder: mobileTwakeContextMenuBuilder, controller: TextEditingController(text: key), decoration: const InputDecoration( contentPadding: EdgeInsets.all(16), @@ -261,6 +263,7 @@ class BootstrapDialogState extends State { maxLines: 2, autocorrect: false, readOnly: _recoveryKeyInputLoading, + contextMenuBuilder: mobileTwakeContextMenuBuilder, autofillHints: _recoveryKeyInputLoading ? null : [AutofillHints.password], diff --git a/lib/pages/bootstrap/tom_bootstrap_dialog_mobile_view.dart b/lib/pages/bootstrap/tom_bootstrap_dialog_mobile_view.dart index 50a94fc7c5..0f8ded0a9d 100644 --- a/lib/pages/bootstrap/tom_bootstrap_dialog_mobile_view.dart +++ b/lib/pages/bootstrap/tom_bootstrap_dialog_mobile_view.dart @@ -3,6 +3,7 @@ import 'package:fluffychat/pages/bootstrap/tom_bootstrap_dialog_style.dart'; import 'package:fluffychat/pages/chat_list/chat_list_header_style.dart'; import 'package:fluffychat/pages/chat_list/chat_list_skeletonizer_widget.dart'; import 'package:fluffychat/resource/image_paths.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:skeletonizer/skeletonizer.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -55,6 +56,7 @@ class TomBootstrapDialogMobileView extends StatelessWidget { child: TextField( textInputAction: TextInputAction.search, enabled: false, + contextMenuBuilder: mobileTwakeContextMenuBuilder, decoration: ChatListHeaderStyle.searchInputDecoration( context, hintText: '', diff --git a/lib/pages/chat/add_widget_tile_view.dart b/lib/pages/chat/add_widget_tile_view.dart index d7ac53ef2b..18153fabaa 100644 --- a/lib/pages/chat/add_widget_tile_view.dart +++ b/lib/pages/chat/add_widget_tile_view.dart @@ -59,7 +59,7 @@ class AddWidgetTileView extends StatelessWidget { ), ), ), - ButtonBar( + OverflowBar( children: [ TextButton( onPressed: controller.addWidget, diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 089441c617..db897f0583 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -19,7 +19,6 @@ import 'package:fluffychat/widgets/mixins/popup_menu_widget_style.dart'; import 'package:fluffychat/widgets/mixins/twake_context_menu_mixin.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:fluffychat/utils/extension/global_key_extension.dart'; -import 'package:inview_notifier_list/inview_notifier_list.dart'; import 'package:universal_html/html.dart' as html; import 'package:adaptive_dialog/adaptive_dialog.dart'; @@ -388,11 +387,20 @@ class ChatController extends State if (scrollController.position.pixels == 0 || scrollController.position.pixels == _isPortionAvailableToScroll) { requestFuture(); - } else if (scrollController.position.pixels == + } + + _handleRequestHistory(); + } + + void _handleRequestHistory() { + if (scrollController.position.pixels == scrollController.position.maxScrollExtent || scrollController.position.pixels + _isPortionAvailableToScroll == scrollController.position.maxScrollExtent) { - await requestHistory(); + if (timeline?.isRequestingHistory == true) return; + if (timeline?.canRequestHistory == true) { + requestHistory(); + } } } @@ -828,7 +836,7 @@ class ChatController extends State ); } _clearSelectEvent(); - context.go( + context.push( '/rooms/forward', extra: ForwardArgument( fromRoomId: roomId ?? '', @@ -934,6 +942,7 @@ class ChatController extends State if (timeline == null) return; if (!timeline!.allowNewEvent) { setState(() { + timeline = null; loadTimelineFuture = _getTimeline().onError( (e, s) { Logs().e('Chat::scrollDown(): Unable to load timeline', e, s); @@ -954,26 +963,42 @@ class ChatController extends State return eventIndex + addedHeadItemsInChat; } - Future scrollToEventId(String eventId, {bool highlight = false}) async { - final eventIndex = timeline!.events.indexWhere((e) => e.eventId == eventId); + int _getEventIndex(String eventId) { + final foundEvent = + timeline!.events.firstWhereOrNull((event) => event.eventId == eventId); + + final eventIndex = foundEvent == null + ? -1 + : timeline!.events.indexWhere( + (event) => event.eventId == foundEvent.eventId, + ); + + return eventIndex; + } + + Future scrollToEventId(String eventId, {bool highlight = true}) async { + final eventIndex = _getEventIndex(eventId); if (eventIndex == -1) { - loadTimelineFuture = _getTimeline(eventContextId: eventId).onError( - (e, s) { - Logs().e('Chat::scrollToEventId(): Unable to load timeline', e, s); - }, - ); + setState(() { + timeline = null; + loadTimelineFuture = _getTimeline(eventContextId: eventId).onError( + (e, s) { + Logs().e('Chat::scrollToEventId(): Unable to load timeline', e, s); + }, + ); + }); await loadTimelineFuture; WidgetsBinding.instance.addPostFrameCallback((timeStamp) { scrollToEventId(eventId, highlight: highlight); }); - setState(() {}); return; } + await scrollToIndex(getDisplayEventIndex(eventIndex), highlight: highlight); _updateScrollController(); } - Future scrollToIndex(int index, {bool highlight = false}) async { + Future scrollToIndex(int index, {bool highlight = true}) async { await scrollController.scrollToIndex( index, preferPosition: AutoScrollPosition.middle, @@ -2001,7 +2026,6 @@ class ChatController extends State pinnedMessageScrollController.dispose(); onUpdateEventStreamSubcription?.cancel(); keyboardVisibilitySubscription?.cancel(); - InViewNotifierListCustom.of(context)?.dispose(); replyEventNotifier.dispose(); cachedPresenceStreamController.close(); cachedPresenceNotifier.dispose(); diff --git a/lib/pages/chat/chat_app_bar_title.dart b/lib/pages/chat/chat_app_bar_title.dart index cb47435d7c..0e61b020ed 100644 --- a/lib/pages/chat/chat_app_bar_title.dart +++ b/lib/pages/chat/chat_app_bar_title.dart @@ -91,10 +91,11 @@ class ChatAppBarTitle extends StatelessWidget { padding: const EdgeInsets.all(2.0), child: SvgPicture.asset( ImagePaths.icEncrypted, - width: 20, - height: 20, + width: 16, + height: 16, ), ), + const SizedBox(width: 4), Flexible( child: Text( roomName ?? @@ -196,7 +197,7 @@ class _DirectChatAppBarStatusContent extends StatelessWidget { } if (directChatPresence == null) { return ChatAppBarTitleText( - text: L10n.of(context)!.loading, + text: L10n.of(context)!.loadingStatus, ); } final typingText = room.getLocalizedTypingText(context); diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 6592c74fcd..4a01430949 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -9,7 +9,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:inview_notifier_list/inview_notifier_list.dart'; -import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; @@ -150,7 +149,7 @@ class ChatEventList extends StatelessWidget { key: ValueKey(event.eventId), index: index, controller: controller.scrollController, - highlightColor: LinagoraRefColors.material().primary[99], + highlightColor: Theme.of(context).highlightColor, child: event.isVisibleInGui ? Message( key: ValueKey(event.eventId), @@ -168,7 +167,10 @@ class ChatEventList extends StatelessWidget { onSelect: controller.onSelectMessage, selectMode: controller.selectMode, scrollToEventId: (String eventId) => - controller.scrollToEventId(eventId), + controller.scrollToEventId( + eventId, + highlight: true, + ), selected: controller.selectedEvents .any((e) => e.eventId == event.eventId), timeline: controller.timeline!, diff --git a/lib/pages/chat/chat_pinned_events/pinned_events_controller.dart b/lib/pages/chat/chat_pinned_events/pinned_events_controller.dart index 9b3e08745f..bf47cdb744 100644 --- a/lib/pages/chat/chat_pinned_events/pinned_events_controller.dart +++ b/lib/pages/chat/chat_pinned_events/pinned_events_controller.dart @@ -79,21 +79,27 @@ class PinnedEventsController { void Function(String)? scrollToEventId, }) async { final nextIndex = _nextIndexOfPinnedMessage(pinnedEvents); - final event = pinnedEvents[nextIndex]; + final nextEvent = pinnedEvents[nextIndex]; + final currentEvent = currentPinnedEventNotifier.value; Logs().d( - "PinnedEventsController()::jumpToPinnedMessage(): eventID: ${event?.eventId}", + "PinnedEventsController()::jumpToPinnedMessage(): eventID: ${nextEvent?.eventId}", ); - if (event != null) { - currentPinnedEventNotifier.value = event; + if (currentEvent != null) { + currentPinnedEventNotifier.value = nextEvent; if (scrollToEventId != null) { - scrollToEventId.call(event.eventId); + scrollToEventId.call(currentEvent.eventId); } } } int currentIndexOfPinnedMessage(List pinnedEvents) { final index = pinnedEvents.indexWhere( - (event) => event?.eventId == currentPinnedEventNotifier.value?.eventId, + (event) { + Logs().d( + "PinnedEventsController()::currentIndexOfPinnedMessage(): ${currentPinnedEventNotifier.value?.eventId}", + ); + return event?.eventId == currentPinnedEventNotifier.value?.eventId; + }, ); if (index < 0) { currentPinnedEventNotifier.value = pinnedEvents.first; diff --git a/lib/pages/chat/chat_pinned_events/pinned_events_view.dart b/lib/pages/chat/chat_pinned_events/pinned_events_view.dart index 8443f59353..39bb3b31a7 100644 --- a/lib/pages/chat/chat_pinned_events/pinned_events_view.dart +++ b/lib/pages/chat/chat_pinned_events/pinned_events_view.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/domain/app_state/room/chat_get_pinned_events_state.da import 'package:fluffychat/pages/chat/chat_pinned_events/pinned_events_argument.dart'; import 'package:fluffychat/pages/chat/chat_pinned_events/pinned_events_style.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; @@ -187,6 +188,7 @@ class _PinnedEventsIndicator extends StatelessWidget { currentEvent.eventId, ), index: index, + highlightColor: Theme.of(context).highlightColor, controller: scrollController, child: Container( width: PinnedEventsStyle.maxWidthIndicator, @@ -230,6 +232,22 @@ class _PinnedEventsContentWidget extends StatelessWidget { hideReply: true, ), builder: (context, snapshot) { + if (currentEvent.isAFile) { + return LinkText( + text: currentEvent.filename, + maxLines: 1, + textStyle: + LinagoraTextStyle.material().bodyMedium3.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurfaceVariant, + overflow: TextOverflow.ellipsis, + decoration: currentEvent.redacted + ? TextDecoration.lineThrough + : null, + ), + ); + } return LinkText( text: snapshot.data ?? currentEvent.calcLocalizedBodyFallback( diff --git a/lib/pages/chat/events/images_builder/sending_image_info_widget.dart b/lib/pages/chat/events/images_builder/sending_image_info_widget.dart index 3e55151aa4..874b38a202 100644 --- a/lib/pages/chat/events/images_builder/sending_image_info_widget.dart +++ b/lib/pages/chat/events/images_builder/sending_image_info_widget.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; import 'package:fluffychat/pages/image_viewer/image_viewer.dart'; +import 'package:fluffychat/presentation/model/chat/upload_file_ui_state.dart'; import 'package:fluffychat/presentation/model/file/display_image_info.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/interactive_viewer_gallery.dart'; @@ -87,14 +88,23 @@ class _SendingImageInfoWidgetState extends State strokeWidth: 2, color: LinagoraRefColors.material().primary[100], ), - InkWell( - child: Icon( - Icons.close, - color: LinagoraRefColors.material().primary[100], - ), - onTap: () { - uploadManager.cancelUpload(widget.event); + ValueListenableBuilder( + valueListenable: uploadFileStateNotifier, + builder: (context, state, child) { + if (state is UploadFileSuccessUIState) { + return child!; + } + return InkWell( + child: Icon( + Icons.close, + color: LinagoraRefColors.material().primary[100], + ), + onTap: () { + uploadManager.cancelUpload(widget.event); + }, + ); }, + child: const SizedBox.shrink(), ), ], ], diff --git a/lib/pages/chat/events/message/message.dart b/lib/pages/chat/events/message/message.dart index 6790e63dc8..1a471eb29e 100644 --- a/lib/pages/chat/events/message/message.dart +++ b/lib/pages/chat/events/message/message.dart @@ -258,6 +258,7 @@ class _MessageState extends State { constraints: BoxConstraints( maxWidth: ChatViewBodyStyle.chatScreenMaxWidth, ), + padding: MessageStyle.paddingMessage, alignment: Alignment.bottomCenter, child: SwipeableMessage( event: widget.event, diff --git a/lib/pages/chat/events/message/message_style.dart b/lib/pages/chat/events/message/message_style.dart index 0ebce83e8c..3743ef5774 100644 --- a/lib/pages/chat/events/message/message_style.dart +++ b/lib/pages/chat/events/message/message_style.dart @@ -92,6 +92,9 @@ class MessageStyle { bottom: 4.0, ); + static EdgeInsets get paddingMessage => + const EdgeInsets.symmetric(vertical: 2.0); + static EdgeInsets get paddingTimestamp => const EdgeInsets.only( left: 8.0, right: 4.0, diff --git a/lib/pages/chat/events/message_upload_content.dart b/lib/pages/chat/events/message_upload_content.dart index e758799624..54e4161c00 100644 --- a/lib/pages/chat/events/message_upload_content.dart +++ b/lib/pages/chat/events/message_upload_content.dart @@ -112,6 +112,9 @@ class _MessageUploadingContentState extends State ), InkWell( onTap: () { + if (uploadFileState is UploadFileSuccessUIState) { + return; + } uploadManager.cancelUpload(event); }, mouseCursor: SystemMouseCursors.click, diff --git a/lib/pages/chat/events/message_video_upload_content.dart b/lib/pages/chat/events/message_video_upload_content.dart index e50bf0f137..0c6fdaa434 100644 --- a/lib/pages/chat/events/message_video_upload_content.dart +++ b/lib/pages/chat/events/message_video_upload_content.dart @@ -49,6 +49,9 @@ class _MessageVideoUploadContentWebState width: widget.width, height: widget.height, onVideoTapped: () { + if (uploadState is UploadFileSuccessUIState) { + return; + } uploadManager.cancelUpload(event); }, centerWidget: Stack( @@ -58,10 +61,13 @@ class _MessageVideoUploadContentWebState width: MessageContentStyle.videoCenterButtonSize, height: MessageContentStyle.videoCenterButtonSize, ), - const CenterVideoButton( - icon: Icons.close, - iconSize: MessageContentStyle.cancelButtonSize, - ), + if (uploadState is UploadFileSuccessUIState) ...[ + const SizedBox.shrink(), + ] else + const CenterVideoButton( + icon: Icons.close, + iconSize: MessageContentStyle.cancelButtonSize, + ), SizedBox( width: MessageContentStyle.iconInsideVideoButtonSize, height: MessageContentStyle.iconInsideVideoButtonSize, diff --git a/lib/pages/chat/events/reply_content.dart b/lib/pages/chat/events/reply_content.dart index 0188a559c8..ece7623140 100644 --- a/lib/pages/chat/events/reply_content.dart +++ b/lib/pages/chat/events/reply_content.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/events/reply_content_style.dart'; +import 'package:fluffychat/resource/image_paths.dart'; import 'package:fluffychat/utils/extension/event_info_extension.dart'; import 'package:fluffychat/utils/extension/mime_type_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; @@ -51,6 +52,13 @@ class ReplyContent extends StatelessWidget { room: displayEvent.room, emoteSize: ReplyContentStyle.fontSizeDisplayContent * 1.5, ); + } else if (displayEvent.isAFile) { + replyBody = Text( + displayEvent.filename, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: ReplyContentStyle.replyBodyTextStyle(context), + ); } else { replyBody = Text( displayEvent.calcLocalizedBodyFallback( @@ -152,6 +160,16 @@ class ReplyPreviewIconBuilder extends StatelessWidget { height: ReplyContentStyle.replyContentSize, ); } + if (event.isAFile) { + return SvgPicture.asset( + event.mimeType?.getIcon( + fileType: event.fileType, + ) ?? + ImagePaths.icFileUnknown, + width: ReplyContentStyle.replyContentSize, + height: ReplyContentStyle.replyContentSize, + ); + } return ClipRRect( borderRadius: ReplyContentStyle.previewedImageBorderRadius, child: MxcImage( diff --git a/lib/pages/chat/events/sending_video_widget.dart b/lib/pages/chat/events/sending_video_widget.dart index dac779a2e5..11efd6a969 100644 --- a/lib/pages/chat/events/sending_video_widget.dart +++ b/lib/pages/chat/events/sending_video_widget.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/pages/chat/events/message_content_style.dart'; import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; +import 'package:fluffychat/presentation/model/chat/upload_file_ui_state.dart'; import 'package:fluffychat/presentation/model/file/display_image_info.dart'; import 'package:fluffychat/widgets/mixins/upload_file_mixin.dart'; import 'package:flutter/material.dart'; @@ -61,18 +62,26 @@ class _SendingVideoWidgetState extends State _PlayVideoButton( event: widget.event, ), - InkWell( - onTap: () { - uploadManager.cancelUpload(widget.event); + ValueListenableBuilder( + valueListenable: uploadFileStateNotifier, + builder: (context, state, child) { + return InkWell( + onTap: () { + if (state is UploadFileSuccessUIState) { + return; + } + uploadManager.cancelUpload(widget.event); + }, + child: SizedBox( + width: MessageContentStyle.videoCenterButtonSize, + height: MessageContentStyle.videoCenterButtonSize, + child: CircularProgressIndicator( + strokeWidth: MessageContentStyle.strokeVideoWidth, + color: LinagoraRefColors.material().primary[100], + ), + ), + ); }, - child: SizedBox( - width: MessageContentStyle.videoCenterButtonSize, - height: MessageContentStyle.videoCenterButtonSize, - child: CircularProgressIndicator( - strokeWidth: MessageContentStyle.strokeVideoWidth, - color: LinagoraRefColors.material().primary[100], - ), - ), ), ], ), diff --git a/lib/pages/chat/input_bar/input_bar.dart b/lib/pages/chat/input_bar/input_bar.dart index 1ebac66e21..2835cf3abe 100644 --- a/lib/pages/chat/input_bar/input_bar.dart +++ b/lib/pages/chat/input_bar/input_bar.dart @@ -12,6 +12,7 @@ import 'package:fluffychat/presentation/mixins/paste_image_mixin.dart'; import 'package:fluffychat/utils/clipboard.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; @@ -445,12 +446,8 @@ class _InputBarState extends State with PasteImageMixin { widget.onChanged!(text); } }, - contextMenuBuilder: PlatformInfos.isWeb - ? null - : (_, editableTextState) => - AdaptiveTextSelectionToolbar.editableText( - editableTextState: editableTextState, - ), + contextMenuBuilder: + PlatformInfos.isWeb ? null : mobileTwakeContextMenuBuilder, onTap: () async { await Future.delayed(InputBar.debounceDurationTap); FocusScope.of(context).requestFocus(focusNode); diff --git a/lib/pages/chat_details/chat_details_edit.dart b/lib/pages/chat_details/chat_details_edit.dart index ef44388ddb..bb5534c47b 100644 --- a/lib/pages/chat_details/chat_details_edit.dart +++ b/lib/pages/chat_details/chat_details_edit.dart @@ -136,7 +136,7 @@ class ChatDetailsEditController extends State _getImageOnWeb(context); return; } - final currentPermissionPhotos = await getCurrentMediaPermission(); + final currentPermissionPhotos = await getCurrentMediaPermission(context); if (currentPermissionPhotos != null) { final imagePickerController = createImagePickerController(); showImagePickerBottomSheet( diff --git a/lib/pages/chat_details/chat_details_edit_view.dart b/lib/pages/chat_details/chat_details_edit_view.dart index 5f64d3bd85..9ea1802228 100644 --- a/lib/pages/chat_details/chat_details_edit_view.dart +++ b/lib/pages/chat_details/chat_details_edit_view.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/pages/chat_details/chat_details_edit_view_style.dart' import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; @@ -319,6 +320,7 @@ class _GroupNameField extends StatelessWidget { child: TextField( style: ChatDetailEditViewStyle.textFieldStyle(context), controller: controller.groupNameTextEditingController, + contextMenuBuilder: mobileTwakeContextMenuBuilder, focusNode: controller.groupNameFocusNode, decoration: InputDecoration( border: OutlineInputBorder( @@ -370,6 +372,7 @@ class _DescriptionField extends StatelessWidget { TextField( style: ChatDetailEditViewStyle.textFieldStyle(context), controller: controller.descriptionTextEditingController, + contextMenuBuilder: mobileTwakeContextMenuBuilder, focusNode: controller.descriptionFocusNode, decoration: InputDecoration( border: OutlineInputBorder( diff --git a/lib/pages/chat_details/participant_list_item/participant_list_item.dart b/lib/pages/chat_details/participant_list_item/participant_list_item.dart index e059fa7509..6f8150a01b 100644 --- a/lib/pages/chat_details/participant_list_item/participant_list_item.dart +++ b/lib/pages/chat_details/participant_list_item/participant_list_item.dart @@ -186,11 +186,11 @@ class ParticipantListItem extends StatelessWidget { Icons.person_search, color: LinagoraSysColors.material().onSurface, ), - label: L10n.of(context)?.viewProfile != null + label: L10n.of(bottomSheetContext)?.viewProfile != null ? Row( children: [ Text( - L10n.of(context)!.viewProfile, + L10n.of(bottomSheetContext)!.viewProfile, style: TextStyle( color: LinagoraSysColors.material() .onSurface, diff --git a/lib/pages/chat_draft/draft_chat.dart b/lib/pages/chat_draft/draft_chat.dart index 7fb0a90f74..7ffad50c3c 100644 --- a/lib/pages/chat_draft/draft_chat.dart +++ b/lib/pages/chat_draft/draft_chat.dart @@ -340,7 +340,8 @@ class DraftChatController extends State Room? room, }) async { final result = await FilePicker.platform.pickFiles( - withData: true, + withReadStream: true, + allowMultiple: true, ); if (result == null || result.files.isEmpty) return; @@ -357,15 +358,6 @@ class DraftChatController extends State BuildContext context, List matrixFilesList, ) async { - const int maxFileQuantity = 1; - if (matrixFilesList.length > maxFileQuantity) { - TwakeSnackBar.show( - context, - L10n.of(context)!.countFilesSendPerDialog(maxFileQuantity), - ); - return; - } - if (matrixFilesList.isEmpty) { TwakeSnackBar.show( context, @@ -376,7 +368,7 @@ class DraftChatController extends State final dialogStatus = await sendImagesWithCaption( context: context, - matrixFiles: [matrixFilesList.first], + matrixFiles: matrixFilesList, ); if (dialogStatus is SendMediaWithCaptionStatus) { diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index b3511394dc..1d222ed917 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -788,7 +788,6 @@ class ChatListController extends State WidgetsBinding.instance.addPostFrameCallback((_) async { if (mounted) { Matrix.of(context).backgroundPush?.setupPush(); - await matrixState.retrievePersistedActiveClient(); } }); _checkTorBrowser(); diff --git a/lib/pages/chat_list/chat_list_body_view.dart b/lib/pages/chat_list/chat_list_body_view.dart index 86c8767d37..d946d4c341 100644 --- a/lib/pages/chat_list/chat_list_body_view.dart +++ b/lib/pages/chat_list/chat_list_body_view.dart @@ -7,7 +7,6 @@ import 'package:fluffychat/pages/chat_list/chat_list_view_builder.dart'; import 'package:fluffychat/pages/chat_list/space_view.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; import 'package:fluffychat/resource/image_paths.dart'; -import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/connection_status_header.dart'; import 'package:flutter/material.dart'; @@ -148,15 +147,7 @@ class ChatListBodyView extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - ExpandableTitleBuilder( - title: L10n.of(context)!.countPinChat( - controller.filteredRoomsForPin.length, - ), - isExpanded: isExpanded, - onTap: controller - .expandRoomsForPinNotifier.toggle, - ), - if (isExpanded) child!, + child!, ], ); }, @@ -166,33 +157,15 @@ class ChatListBodyView extends StatelessWidget { ), ), if (!controller.filteredRoomsForAllIsEmpty) - ValueListenableBuilder( - valueListenable: controller.expandRoomsForAllNotifier, - builder: (context, isExpanded, child) { - return Padding( - padding: ChatListBodyViewStyle - .paddingTopExpandableTitleBuilder, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ExpandableTitleBuilder( - title: L10n.of(context)!.countAllChat( - controller.filteredRoomsForAll.length, - ), - isExpanded: isExpanded, - onTap: controller - .expandRoomsForAllNotifier.toggle, - ), - if (isExpanded) child!, - ], - ), - ); - }, - child: ChatListViewBuilder( - controller: controller, - rooms: controller.filteredRoomsForAll, - ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ChatListViewBuilder( + controller: controller, + rooms: controller.filteredRoomsForAll, + ), + ], ), ], ), diff --git a/lib/pages/chat_list/chat_list_header.dart b/lib/pages/chat_list/chat_list_header.dart index 55e463585a..8f8f1ecf2e 100644 --- a/lib/pages/chat_list/chat_list_header.dart +++ b/lib/pages/chat_list/chat_list_header.dart @@ -3,10 +3,13 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/pages/chat_list/chat_list_header_style.dart'; import 'package:fluffychat/pages/search/search.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:fluffychat/widgets/swipe_to_dismiss_wrap.dart'; import 'package:fluffychat/widgets/twake_components/twake_header.dart'; import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_state_layer.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; class ChatListHeader extends StatelessWidget { final ChatListController controller; @@ -31,12 +34,22 @@ class ChatListHeader extends StatelessWidget { onClickAvatar: controller.onClickAvatar, ), Container( + color: ChatListHeaderStyle.responsive.isMobile(context) + ? LinagoraSysColors.material().background + : Colors.transparent, height: ChatListHeaderStyle.searchBarContainerHeight, padding: ChatListHeaderStyle.searchInputPadding, child: PlatformInfos.isWeb ? _normalModeWidgetWeb(context) : _normalModeWidgetsMobile(context), ), + if (ChatListHeaderStyle.responsive.isMobile(context)) + Divider( + height: ChatListHeaderStyle.dividerHeight, + thickness: ChatListHeaderStyle.dividerThickness, + color: LinagoraStateLayer(LinagoraSysColors.material().surfaceTint) + .opacityLayer3, + ), ], ); } @@ -83,6 +96,7 @@ class ChatListHeader extends StatelessWidget { builder: (context, value, _) { return TextField( textInputAction: TextInputAction.search, + contextMenuBuilder: mobileTwakeContextMenuBuilder, enabled: false, decoration: ChatListHeaderStyle.searchInputDecoration( context, diff --git a/lib/pages/chat_list/chat_list_header_style.dart b/lib/pages/chat_list/chat_list_header_style.dart index bdbaa9ee18..749002c447 100644 --- a/lib/pages/chat_list/chat_list_header_style.dart +++ b/lib/pages/chat_list/chat_list_header_style.dart @@ -9,13 +9,14 @@ class ChatListHeaderStyle { static ResponsiveUtils responsive = getIt.get(); static const double searchRadiusBorder = 24.0; - static const double searchBarContainerHeight = 64.0; + static const double searchBarContainerHeight = 57.0; static const double searchIconSize = 24.0; static const EdgeInsetsDirectional searchInputPadding = EdgeInsetsDirectional.only( start: 16, end: 16, + bottom: 8, ); static const EdgeInsetsDirectional paddingZero = EdgeInsetsDirectional.zero; @@ -41,11 +42,14 @@ class ChatListHeaderStyle { ), floatingLabelBehavior: FloatingLabelBehavior.never, prefixIcon: Icon( - Icons.search_outlined, + Icons.search, size: ChatListHeaderStyle.searchIconSize, - color: prefixIconColor ?? Theme.of(context).colorScheme.onSurface, + color: prefixIconColor ?? LinagoraRefColors.material().neutral[60], ), suffixIcon: const SizedBox.shrink(), ); } + + static const dividerHeight = 1.0; + static const dividerThickness = 1.0; } diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index e1c78d5e74..15ef3afa99 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -1,5 +1,4 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/presentation/mixins/chat_list_item_mixin.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item_style.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item_subtitle.dart'; @@ -91,66 +90,86 @@ class ChatListItem extends StatelessWidget with ChatListItemMixin { MatrixLocals(L10n.of(context)!), ); return Padding( - padding: ChatListItemStyle.paddingConversation, + padding: ChatListItemStyle.padding, child: Material( - borderRadius: BorderRadius.circular(AppConfig.borderRadius), + borderRadius: ChatListItemStyle.chatlistItemBorderRadius, clipBehavior: Clip.hardEdge, color: isSelectedItem ? Theme.of(context).colorScheme.primaryContainer : activeChat ? Theme.of(context).colorScheme.secondaryContainer : Colors.transparent, - child: InkWell( - onTap: () => clickAction(context), - onSecondaryTapDown: onSecondaryTapDown, - onLongPress: onLongPress, - child: Container( - height: ChatListItemStyle.chatItemHeight, - padding: ChatListItemStyle.paddingBody, - child: Row( - children: [ - if (isEnableSelectMode) checkBoxWidget ?? const SizedBox(), - Padding( - padding: ChatListItemStyle.paddingAvatar, - child: Stack( - children: [ - Avatar( - mxContent: room.avatar, - name: displayName, - onTap: onTapAvatar, - ), - if (_isGroupChat) - Positioned( - bottom: 0, - right: 0, - child: Container( - padding: ChatListItemStyle.paddingIconGroup, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).colorScheme.onPrimary, - ), - child: Icon( - Icons.group, - size: ChatListItemStyle.groupIconSize, - color: room.isUnreadOrInvited - ? LinagoraSysColors.material() - .onSurfaceVariant - : LinagoraRefColors.material().tertiary[30], + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: + LinagoraStateLayer(LinagoraSysColors.material().surfaceTint) + .opacityLayer3, + width: ChatListItemStyle.chatListBottomBorderWidht, + ), + ), + ), + child: InkWell( + onTap: () => clickAction(context), + onSecondaryTapDown: onSecondaryTapDown, + onLongPress: onLongPress, + borderRadius: ChatListItemStyle.chatlistItemBorderRadius, + child: Container( + height: ChatListItemStyle.chatItemHeight, + padding: ChatListItemStyle.paddingBody, + decoration: BoxDecoration( + borderRadius: ChatListItemStyle.chatlistItemBorderRadius, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (isEnableSelectMode) checkBoxWidget ?? const SizedBox(), + Padding( + padding: ChatListItemStyle.paddingAvatar, + child: Stack( + children: [ + Avatar( + mxContent: room.avatar, + name: displayName, + onTap: onTapAvatar, + ), + if (_isGroupChat) + Positioned( + bottom: 0, + right: 0, + child: Container( + padding: ChatListItemStyle.paddingIconGroup, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).colorScheme.onPrimary, + ), + child: Icon( + Icons.group, + size: ChatListItemStyle.groupIconSize, + color: room.isUnreadOrInvited + ? LinagoraSysColors.material() + .onSurfaceVariant + : LinagoraRefColors.material().tertiary[30], + ), ), ), - ), - ], + ], + ), ), - ), - Expanded( - child: Column( - children: [ - ChatListItemTitle(room: room), - ChatListItemSubtitle(room: room), - ], + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ChatListItemTitle( + room: room, + ), + ChatListItemSubtitle(room: room), + ], + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/pages/chat_list/chat_list_item_style.dart b/lib/pages/chat_list/chat_list_item_style.dart index 56111fe7ca..953b14a99f 100644 --- a/lib/pages/chat_list/chat_list_item_style.dart +++ b/lib/pages/chat_list/chat_list_item_style.dart @@ -5,7 +5,7 @@ class ChatListItemStyle { static Color? get readIconColor => LinagoraRefColors.material().tertiary[20]; static Color? get pinnedIconColor => - LinagoraRefColors.material().tertiary[30]; + LinagoraRefColors.material().tertiary[40]; static const double readIconSize = 20; @@ -13,7 +13,7 @@ class ChatListItemStyle { static const double mentionIconWidth = 20; - static const double chatItemHeight = 85; + static const double chatItemHeight = 80; static double unreadBadgeSize( bool unread, @@ -27,14 +27,19 @@ class ChatListItemStyle { : 0.0; } - static const EdgeInsetsDirectional paddingConversation = - EdgeInsetsDirectional.symmetric( + static const EdgeInsets paddingConversation = EdgeInsets.fromLTRB( + 8, + 8, + 8, + 8, + ); + + static const EdgeInsets padding = EdgeInsets.symmetric( horizontal: 8, - vertical: 2, ); static const EdgeInsetsDirectional paddingAvatar = - EdgeInsetsDirectional.only(end: 8); + EdgeInsetsDirectional.only(start: 8, end: 8); static const EdgeInsetsDirectional paddingIconGroup = EdgeInsetsDirectional.all(4); @@ -57,4 +62,10 @@ class ChatListItemStyle { } static const double letterSpaceDisplayName = 0.15; + + static final chatlistItemBorderRadius = BorderRadius.circular(4); + + static const paddingIcon = EdgeInsets.only(bottom: 4); + + static const chatListBottomBorderWidht = 1.0; } diff --git a/lib/pages/chat_list/chat_list_item_subtitle.dart b/lib/pages/chat_list/chat_list_item_subtitle.dart index af3b8afe30..e392b86739 100644 --- a/lib/pages/chat_list/chat_list_item_subtitle.dart +++ b/lib/pages/chat_list/chat_list_item_subtitle.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/domain/model/room/room_extension.dart'; import 'package:fluffychat/presentation/mixins/chat_list_item_mixin.dart'; import 'package:fluffychat/pages/chat_list/chat_list_item_style.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; @@ -9,6 +10,7 @@ import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; import 'package:matrix/matrix.dart'; class ChatListItemSubtitle extends StatelessWidget with ChatListItemMixin { @@ -25,6 +27,15 @@ class ChatListItemSubtitle extends StatelessWidget with ChatListItemMixin { room.hasNewMessages, room.notificationCount > 0, ); + final isMediaEvent = room.lastEvent?.messageType == MessageTypes.Image || + room.lastEvent?.messageType == MessageTypes.Video; + + final haveNotificationsAndMuted = + room.notificationCount > 0 && room.isMuted; + + final haveNotificationsOrUnread = + room.notificationCount > 0 || room.markedUnread; + return Row( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, @@ -32,17 +43,22 @@ class ChatListItemSubtitle extends StatelessWidget with ChatListItemMixin { Expanded( child: typingText.isNotEmpty ? typingTextWidget(typingText, context) - : (isGroup + : isGroup ? chatListItemSubtitleForGroup( context: context, room: room, ) - : textContentWidget( - room, - context, - isGroup, - room.isUnreadOrInvited, - )), + : isMediaEvent + ? chatlistItemMediaPreviewSubTitle( + context, + room, + ) + : textContentWidget( + room, + context, + isGroup, + room.isUnreadOrInvited, + ), ), const SizedBox(width: 8), FutureBuilder( @@ -60,39 +76,49 @@ class ChatListItemSubtitle extends StatelessWidget with ChatListItemMixin { room.lastEvent == null) { return const SizedBox.shrink(); } - final isMentionned = snapshot.data! .getAllMentionedUserIdsFromMessage(room) .contains(Matrix.of(context).client.userID); - return AnimatedContainer( - duration: TwakeThemes.animationDuration, - curve: TwakeThemes.animationCurve, - padding: const EdgeInsets.only(bottom: 4), - height: ChatListItemStyle.mentionIconWidth, - width: isMentionned && room.isUnreadOrInvited - ? ChatListItemStyle.mentionIconWidth - : 0, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - ), - child: Center( - child: isMentionned && room.isUnreadOrInvited - ? Text( - '@', - style: TextStyle( - color: isMentionned - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context) - .colorScheme - .onPrimaryContainer, - fontSize: - Theme.of(context).textTheme.labelMedium?.fontSize, - ), - ) - : Container(), - ), - ); + return room.lastEvent?.senderId == Matrix.of(context).client.userID + ? Icon( + Icons.done_all, + color: room.lastEvent!.receipts.isEmpty + ? LinagoraRefColors.material().tertiary[30] + : LinagoraSysColors.material().secondary, + size: 20, + ) + : AnimatedContainer( + duration: TwakeThemes.animationDuration, + curve: TwakeThemes.animationCurve, + padding: const EdgeInsets.only(bottom: 4), + height: ChatListItemStyle.mentionIconWidth, + width: isMentionned && room.isUnreadOrInvited + ? ChatListItemStyle.mentionIconWidth + : 0, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + ), + child: Center( + child: isMentionned && room.isUnreadOrInvited + ? Text( + '@', + style: TextStyle( + color: isMentionned + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context) + .colorScheme + .onPrimaryContainer, + fontSize: Theme.of(context) + .textTheme + .labelMedium + ?.fontSize, + ), + ) + : Container(), + ), + ); }, ), const SizedBox(width: 4), @@ -110,9 +136,11 @@ class ChatListItemSubtitle extends StatelessWidget with ChatListItemMixin { color: room.highlightCount > 0 || room.membership == Membership.invite ? Theme.of(context).colorScheme.primary - : room.notificationCount > 0 || room.markedUnread - ? Theme.of(context).colorScheme.primary - : LinagoraRefColors.material().tertiary[30], + : haveNotificationsAndMuted + ? LinagoraRefColors.material().tertiary[30] + : haveNotificationsOrUnread + ? Theme.of(context).colorScheme.primary + : LinagoraRefColors.material().tertiary[30], borderRadius: BorderRadius.circular(AppConfig.borderRadius), ), child: Center( diff --git a/lib/pages/chat_list/chat_list_item_title.dart b/lib/pages/chat_list/chat_list_item_title.dart index 0fa59464df..caa5636adc 100644 --- a/lib/pages/chat_list/chat_list_item_title.dart +++ b/lib/pages/chat_list/chat_list_item_title.dart @@ -16,14 +16,11 @@ import 'package:matrix/matrix.dart'; class ChatListItemTitle extends StatelessWidget with ChatListItemMixin { final Room room; - final TextStyle? textStyle; - final DateTime? originServerTs; const ChatListItemTitle({ super.key, required this.room, - this.textStyle, this.originServerTs, }); @@ -40,26 +37,28 @@ class ChatListItemTitle extends StatelessWidget with ChatListItemMixin { children: [ Row( children: [ + Flexible( + child: Padding( + padding: ChatListItemTitleStyle.paddingRightTitle, + child: Text( + displayName, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: + ChatLitTitleTextStyleView.textStyle.textStyle(room), + ), + ), + ), if (room.encrypted) Padding( - padding: - const EdgeInsets.only(right: 4, top: 2, bottom: 2), + padding: ChatListItemTitleStyle.paddingLeftIcon, child: SvgPicture.asset( ImagePaths.icEncrypted, - width: 20, - height: 20, + width: ChatListItemTitleStyle.encryptedInconWidth, + height: ChatListItemTitleStyle.encryptedInconHeight, ), ), - Flexible( - child: Text( - displayName, - overflow: TextOverflow.ellipsis, - maxLines: 1, - softWrap: false, - style: textStyle ?? - ChatLitTitleTextStyleView.textStyle.textStyle(room), - ), - ), if (room.isFavourite) Padding( padding: ChatListItemTitleStyle.paddingLeftIcon, @@ -90,7 +89,7 @@ class ChatListItemTitle extends StatelessWidget with ChatListItemMixin { if (room.isTypingText(context)) ...[ Icon( Icons.schedule, - color: LinagoraRefColors.material().neutral[50], + color: LinagoraRefColors.material().tertiary[30], size: ChatListItemTitleStyle.iconScheduleSize, ), ], @@ -100,9 +99,7 @@ class ChatListItemTitle extends StatelessWidget with ChatListItemMixin { (originServerTs ?? room.timeCreated) .localizedTimeShort(context), style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: room.isUnreadOrInvited - ? Theme.of(context).colorScheme.onSurface - : LinagoraRefColors.material().neutral[50], + color: LinagoraRefColors.material().tertiary[30], ), ), ), diff --git a/lib/pages/chat_list/chat_list_item_title_style.dart b/lib/pages/chat_list/chat_list_item_title_style.dart index f6513101e2..d802d61c4c 100644 --- a/lib/pages/chat_list/chat_list_item_title_style.dart +++ b/lib/pages/chat_list/chat_list_item_title_style.dart @@ -7,4 +7,10 @@ class ChatListItemTitleStyle { EdgeInsetsDirectional.only( start: 4, ); + static const EdgeInsetsDirectional paddingRightTitle = + EdgeInsetsDirectional.only( + end: 3, + ); + static const encryptedInconHeight = 16.0; + static const encryptedInconWidth = 14.0; } diff --git a/lib/pages/chat_list/chat_list_view_builder.dart b/lib/pages/chat_list/chat_list_view_builder.dart index eee9f8b109..cbc8eb277f 100644 --- a/lib/pages/chat_list/chat_list_view_builder.dart +++ b/lib/pages/chat_list/chat_list_view_builder.dart @@ -23,6 +23,9 @@ class ChatListViewBuilder extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), itemCount: rooms.length, itemBuilder: (BuildContext context, int index) { + if (index == rooms.length) { + return const SizedBox.shrink(); + } return ValueListenableBuilder( valueListenable: controller.selectModeNotifier, builder: (context, selectMode, _) { diff --git a/lib/pages/chat_list/chat_list_view_style.dart b/lib/pages/chat_list/chat_list_view_style.dart index 6289134c67..54ae368360 100644 --- a/lib/pages/chat_list/chat_list_view_style.dart +++ b/lib/pages/chat_list/chat_list_view_style.dart @@ -38,4 +38,8 @@ class ChatListViewStyle { ? LinagoraRefColors.material().primary[50] : LinagoraRefColors.material().primary[40]; } + + static double dividerHeight = 1.0; + static double dividerIndent = 8.0; + static double dividerThickness = 1.0; } diff --git a/lib/pages/chat_list/receive_sharing_intent_mixin.dart b/lib/pages/chat_list/receive_sharing_intent_mixin.dart index d827a8b5a4..a559dc1154 100644 --- a/lib/pages/chat_list/receive_sharing_intent_mixin.dart +++ b/lib/pages/chat_list/receive_sharing_intent_mixin.dart @@ -1,7 +1,11 @@ +import 'package:app_links/app_links.dart'; import 'package:fluffychat/event/twake_event_types.dart'; +import 'package:fluffychat/pages/share/share.dart'; import 'package:fluffychat/presentation/extensions/shared_media_file_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/url_launcher.dart'; +import 'package:fluffychat/widgets/layouts/agruments/receive_content_args.dart'; +import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; import 'package:fluffychat/widgets/twake_app.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -10,7 +14,6 @@ import 'dart:async'; import 'package:fluffychat/config/app_config.dart'; import 'package:matrix/matrix.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -import 'package:uni_links/uni_links.dart'; mixin ReceiveSharingIntentMixin on State { MatrixState get matrixState; @@ -35,7 +38,28 @@ mixin ReceiveSharingIntentMixin on State { }, ) .toList(); - TwakeApp.router.go('/share'); + openSharePage(); + } + + void openSharePage() { + if (TwakeApp.isCurrentPageIsNotRooms()) { + return; + } + if (TwakeApp.isCurrentPageIsInRooms()) { + TwakeApp.router.go( + '/rooms', + extra: ReceiveContentArgs( + newActiveClient: matrixState.client, + activeDestination: AdaptiveDestinationEnum.rooms, + ), + ); + } + + Navigator.of(TwakeApp.routerKey.currentContext!).push( + MaterialPageRoute( + builder: (context) => const Share(), + ), + ); } void _processIncomingSharedText(String? text) { @@ -53,7 +77,7 @@ mixin ReceiveSharingIntentMixin on State { 'msgtype': 'm.text', 'body': text, }; - TwakeApp.router.go('/share'); + openSharePage(); } void _processIncomingUris(String? text) async { @@ -62,7 +86,7 @@ mixin ReceiveSharingIntentMixin on State { if (_intentOpenApp(text)) { return; } - TwakeApp.router.go('/share'); + openSharePage(); WidgetsBinding.instance.addPostFrameCallback((_) { UrlLauncher(context, url: text).openMatrixToUrl(); }); @@ -86,10 +110,13 @@ mixin ReceiveSharingIntentMixin on State { ReceiveSharingIntent.getInitialText().then(_processIncomingSharedText); // For receiving shared Uris - intentUriStreamSubscription = linkStream.listen(_processIncomingUris); + final appLinks = AppLinks(); + intentUriStreamSubscription = + appLinks.stringLinkStream.listen(_processIncomingUris); + if (TwakeApp.gotInitialLink == false) { TwakeApp.gotInitialLink = true; - getInitialLink().then(_processIncomingUris); + appLinks.getInitialLinkString().then(_processIncomingUris); } } } diff --git a/lib/pages/chat_search/chat_search_view.dart b/lib/pages/chat_search/chat_search_view.dart index 0c5dd2f38b..7876b72842 100644 --- a/lib/pages/chat_search/chat_search_view.dart +++ b/lib/pages/chat_search/chat_search_view.dart @@ -19,6 +19,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/result_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/string_extension.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:fluffychat/widgets/highlight_text.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/search/empty_search_widget.dart'; @@ -343,6 +344,7 @@ class _ChatSearchAppBar extends StatelessWidget { padding: ChatSearchStyle.inputPadding, child: TextField( controller: controller.textEditingController, + contextMenuBuilder: mobileTwakeContextMenuBuilder, focusNode: controller.inputFocus, textInputAction: TextInputAction.search, autofocus: true, diff --git a/lib/pages/contacts_tab/contacts_tab.dart b/lib/pages/contacts_tab/contacts_tab.dart index 7e02a631dd..cf5e4e429b 100644 --- a/lib/pages/contacts_tab/contacts_tab.dart +++ b/lib/pages/contacts_tab/contacts_tab.dart @@ -7,11 +7,12 @@ import 'package:fluffychat/presentation/model/contact/presentation_contact_const import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/utils/string_extension.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; class ContactsTab extends StatefulWidget { @@ -27,7 +28,10 @@ class ContactsTab extends StatefulWidget { } class ContactsTabController extends State - with ComparablePresentationContactMixin, ContactsViewControllerMixin { + with + ComparablePresentationContactMixin, + ContactsViewControllerMixin, + WidgetsBindingObserver { final responsive = getIt.get(); Client get client => Matrix.of(context).client; @@ -35,8 +39,10 @@ class ContactsTabController extends State @override void initState() { SchedulerBinding.instance.addPostFrameCallback((_) async { + WidgetsBinding.instance.addObserver(this); if (mounted) { initialFetchContacts( + context: context, client: Matrix.of(context).client, matrixLocalizations: MatrixLocals(L10n.of(context)!), ); @@ -100,9 +106,15 @@ class ContactsTabController extends State } } + @override + void didChangeAppLifecycleState(AppLifecycleState state) async { + await handleDidChangeAppLifecycleState(state); + } + @override void dispose() { disposeContactsMixin(); + WidgetsBinding.instance.removeObserver(this); super.dispose(); } diff --git a/lib/pages/contacts_tab/contacts_tab_body_view.dart b/lib/pages/contacts_tab/contacts_tab_body_view.dart index 5ea6f239b4..1483528858 100644 --- a/lib/pages/contacts_tab/contacts_tab_body_view.dart +++ b/lib/pages/contacts_tab/contacts_tab_body_view.dart @@ -418,8 +418,8 @@ class _SliverWarningBanner extends StatelessWidget { child: ContactsWarningBannerView( warningBannerNotifier: controller.warningBannerNotifier, closeContactsWarningBanner: controller.closeContactsWarningBanner, - goToSettingsForPermissionActions: - controller.goToSettingsForPermissionActions, + goToSettingsForPermissionActions: () => + controller.displayContactPermissionDialog(context), ), ); } diff --git a/lib/pages/homeserver_picker/homeserver_app_bar.dart b/lib/pages/homeserver_picker/homeserver_app_bar.dart index 81faa0dd8f..1b45a14f78 100644 --- a/lib/pages/homeserver_picker/homeserver_app_bar.dart +++ b/lib/pages/homeserver_picker/homeserver_app_bar.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -15,6 +16,7 @@ class HomeserverAppBar extends StatelessWidget { return TextField( focusNode: controller.homeserverFocusNode, controller: controller.homeserverController, + contextMenuBuilder: mobileTwakeContextMenuBuilder, onChanged: controller.onChanged, decoration: InputDecoration( prefixIcon: Navigator.of(context).canPop() diff --git a/lib/pages/homeserver_picker/homeserver_picker_view.dart b/lib/pages/homeserver_picker/homeserver_picker_view.dart index 0496152e0d..bda3625001 100644 --- a/lib/pages/homeserver_picker/homeserver_picker_view.dart +++ b/lib/pages/homeserver_picker/homeserver_picker_view.dart @@ -1,6 +1,7 @@ import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_state.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:fluffychat/widgets/layouts/login_scaffold.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; @@ -155,6 +156,7 @@ class HomeserverTextField extends StatelessWidget { autocorrect: false, enabled: true, controller: controller.homeserverController, + contextMenuBuilder: mobileTwakeContextMenuBuilder, decoration: InputDecoration( border: OutlineInputBorder( borderSide: diff --git a/lib/pages/login/login_view.dart b/lib/pages/login/login_view.dart index 34993de54a..9ccaf416b5 100644 --- a/lib/pages/login/login_view.dart +++ b/lib/pages/login/login_view.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -40,6 +41,7 @@ class LoginView extends StatelessWidget { autocorrect: false, autofocus: true, onChanged: controller.checkWellKnownWithCoolDown, + contextMenuBuilder: mobileTwakeContextMenuBuilder, controller: controller.usernameController, textInputAction: TextInputAction.next, keyboardType: TextInputType.emailAddress, @@ -60,6 +62,7 @@ class LoginView extends StatelessWidget { autocorrect: false, autofillHints: controller.loading ? null : [AutofillHints.password], + contextMenuBuilder: mobileTwakeContextMenuBuilder, controller: controller.passwordController, textInputAction: TextInputAction.go, obscureText: !controller.showPassword, diff --git a/lib/pages/new_group/contacts_selection.dart b/lib/pages/new_group/contacts_selection.dart index b8cd478738..aeb9a6c8e0 100644 --- a/lib/pages/new_group/contacts_selection.dart +++ b/lib/pages/new_group/contacts_selection.dart @@ -6,14 +6,17 @@ import 'package:fluffychat/pages/new_group/selected_contacts_map_change_notifier import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter/cupertino.dart'; import 'package:matrix/matrix.dart'; abstract class ContactsSelectionController extends State - with InviteExternalContactMixin, ContactsViewControllerMixin { + with + InviteExternalContactMixin, + ContactsViewControllerMixin, + WidgetsBindingObserver { final selectedContactsMapNotifier = SelectedContactsMapChangeNotifier(); String getTitle(BuildContext context); @@ -37,8 +40,10 @@ abstract class ContactsSelectionController @override void initState() { SchedulerBinding.instance.addPostFrameCallback((_) async { + WidgetsBinding.instance.addObserver(this); if (mounted) { initialFetchContacts( + context: context, client: client, matrixLocalizations: MatrixLocals(L10n.of(context)!), ); @@ -47,9 +52,16 @@ abstract class ContactsSelectionController super.initState(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) async { + await handleDidChangeAppLifecycleState(state); + } + @override void dispose() { + WidgetsBinding.instance.removeObserver(this); disposeContactsMixin(); + selectedContactsMapNotifier.dispose(); super.dispose(); } diff --git a/lib/pages/new_group/contacts_selection_view.dart b/lib/pages/new_group/contacts_selection_view.dart index 7c9ac986f4..81bc505041 100644 --- a/lib/pages/new_group/contacts_selection_view.dart +++ b/lib/pages/new_group/contacts_selection_view.dart @@ -54,8 +54,8 @@ class ContactsSelectionView extends StatelessWidget { warningBannerNotifier: controller.warningBannerNotifier, closeContactsWarningBanner: controller.closeContactsWarningBanner, - goToSettingsForPermissionActions: - controller.goToSettingsForPermissionActions, + goToSettingsForPermissionActions: () => + controller.displayContactPermissionDialog(context), ), ), SliverToBoxAdapter( diff --git a/lib/pages/new_group/new_group_chat_info.dart b/lib/pages/new_group/new_group_chat_info.dart index ccdfd22f69..fdb43d036b 100644 --- a/lib/pages/new_group/new_group_chat_info.dart +++ b/lib/pages/new_group/new_group_chat_info.dart @@ -284,7 +284,7 @@ class NewGroupChatInfoController extends State _getImageOnWeb(context); return; } - final currentPermissionPhotos = await getCurrentMediaPermission(); + final currentPermissionPhotos = await getCurrentMediaPermission(context); if (currentPermissionPhotos != null) { final imagePickerController = createImagePickerController(); groupNameFocusNode.unfocus(); diff --git a/lib/pages/new_group/new_group_chat_info_view.dart b/lib/pages/new_group/new_group_chat_info_view.dart index 36a63e2a23..7ce1eae429 100644 --- a/lib/pages/new_group/new_group_chat_info_view.dart +++ b/lib/pages/new_group/new_group_chat_info_view.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/pages/new_group/new_group_chat_info_style.dart'; import 'package:fluffychat/pages/new_group/new_group_info_controller.dart'; import 'package:fluffychat/pages/new_group/widget/expansion_participants_list.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/int_extension.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:fluffychat/widgets/twake_components/twake_fab.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; @@ -228,6 +229,7 @@ class NewGroupChatInfoView extends StatelessWidget { ), contentPadding: NewGroupChatInfoStyle.contentPadding, ), + contextMenuBuilder: mobileTwakeContextMenuBuilder, ); }, ), diff --git a/lib/pages/new_private_chat/new_private_chat.dart b/lib/pages/new_private_chat/new_private_chat.dart index 05140cd85b..f8484b3e75 100644 --- a/lib/pages/new_private_chat/new_private_chat.dart +++ b/lib/pages/new_private_chat/new_private_chat.dart @@ -25,6 +25,7 @@ class NewPrivateChatController extends State ComparablePresentationContactMixin, ContactsViewControllerMixin, GoToDraftChatMixin, + WidgetsBindingObserver, InviteExternalContactMixin, GoToGroupChatMixin { final isShowContactsNotifier = ValueNotifier(true); @@ -34,8 +35,10 @@ class NewPrivateChatController extends State void initState() { super.initState(); SchedulerBinding.instance.addPostFrameCallback((_) async { + WidgetsBinding.instance.addObserver(this); if (mounted) { initialFetchContacts( + context: context, client: Matrix.of(context).client, matrixLocalizations: MatrixLocals(L10n.of(context)!), ); @@ -84,9 +87,16 @@ class NewPrivateChatController extends State }); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) async { + await handleDidChangeAppLifecycleState(state); + } + @override void dispose() { super.dispose(); + WidgetsBinding.instance.removeObserver(this); + isShowContactsNotifier.dispose(); disposeContactsMixin(); scrollController.dispose(); } diff --git a/lib/pages/new_private_chat/new_private_chat_style.dart b/lib/pages/new_private_chat/new_private_chat_style.dart new file mode 100644 index 0000000000..ff06436bc8 --- /dev/null +++ b/lib/pages/new_private_chat/new_private_chat_style.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +class NewPrivateChatStyle { + static const EdgeInsets paddingBody = EdgeInsets.only(left: 8.0, right: 10.0); + + static const EdgeInsets paddingWarningBanner = EdgeInsets.only(top: 16.0); +} diff --git a/lib/pages/new_private_chat/new_private_chat_view.dart b/lib/pages/new_private_chat/new_private_chat_view.dart index 652d19b696..996a8a51e1 100644 --- a/lib/pages/new_private_chat/new_private_chat_view.dart +++ b/lib/pages/new_private_chat/new_private_chat_view.dart @@ -1,8 +1,10 @@ import 'package:fluffychat/pages/new_private_chat/new_private_chat.dart'; +import 'package:fluffychat/pages/new_private_chat/new_private_chat_style.dart'; import 'package:fluffychat/pages/new_private_chat/widget/expansion_list.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/app_bars/searchable_app_bar.dart'; import 'package:fluffychat/widgets/app_bars/searchable_app_bar_style.dart'; +import 'package:fluffychat/widgets/contacts_warning_banner/contacts_warning_banner_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; @@ -31,20 +33,36 @@ class NewPrivateChatView extends StatelessWidget { keyboardDismissBehavior: PlatformInfos.isMobile ? ScrollViewKeyboardDismissBehavior.manual : ScrollViewKeyboardDismissBehavior.onDrag, - padding: const EdgeInsets.only(left: 8.0, right: 10.0), + padding: NewPrivateChatStyle.paddingBody, controller: controller.scrollController, - child: ExpansionList( - presentationContactsNotifier: controller.presentationContactNotifier, - goToNewGroupChat: () => controller.goToNewGroupChat(context), - isShowContactsNotifier: controller.isShowContactsNotifier, - onContactTap: controller.onContactAction, - onExternalContactTap: controller.onExternalContactAction, - toggleContactsList: controller.toggleContactsList, - textEditingController: controller.textEditingController, - warningBannerNotifier: controller.warningBannerNotifier, - closeContactsWarningBanner: controller.closeContactsWarningBanner, - goToSettingsForPermissionActions: - controller.goToSettingsForPermissionActions, + child: Column( + children: [ + Padding( + padding: NewPrivateChatStyle.paddingWarningBanner, + child: ContactsWarningBannerView( + warningBannerNotifier: controller.warningBannerNotifier, + closeContactsWarningBanner: + controller.closeContactsWarningBanner, + goToSettingsForPermissionActions: () => + controller.displayContactPermissionDialog(context), + isShowMargin: false, + ), + ), + ExpansionList( + presentationContactsNotifier: + controller.presentationContactNotifier, + goToNewGroupChat: () => controller.goToNewGroupChat(context), + isShowContactsNotifier: controller.isShowContactsNotifier, + onContactTap: controller.onContactAction, + onExternalContactTap: controller.onExternalContactAction, + toggleContactsList: controller.toggleContactsList, + textEditingController: controller.textEditingController, + warningBannerNotifier: controller.warningBannerNotifier, + closeContactsWarningBanner: controller.closeContactsWarningBanner, + goToSettingsForPermissionActions: () => + controller.displayContactPermissionDialog(context), + ), + ], ), ), ); diff --git a/lib/pages/new_private_chat/widget/contact_status_widget.dart b/lib/pages/new_private_chat/widget/contact_status_widget.dart index e2acf0cfa4..8b959143c1 100644 --- a/lib/pages/new_private_chat/widget/contact_status_widget.dart +++ b/lib/pages/new_private_chat/widget/contact_status_widget.dart @@ -13,36 +13,30 @@ class ContactStatusWidget extends StatelessWidget { required this.status, }); - final Color? activeColor = LinagoraRefColors.material().secondary[40]; final Color? inactiveColor = LinagoraRefColors.material().neutral[60]; @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SvgPicture.asset( - ImagePaths.icStatus, - // ignore: deprecated_member_use - color: status == ContactStatus.active ? activeColor : inactiveColor, - ), - status == ContactStatus.active - ? Text( - " ${L10n.of(context)!.online}", - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: activeColor, - ), - ) - : Text( + return status == ContactStatus.inactive + ? Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SvgPicture.asset( + ImagePaths.icStatus, + colorFilter: + ColorFilter.mode(inactiveColor!, BlendMode.srcIn), + ), + Text( " ${L10n.of(context)!.inactive}", style: Theme.of(context).textTheme.bodySmall?.copyWith( color: inactiveColor, ), ), - ], - ), - ); + ], + ), + ) + : const SizedBox.shrink(); } } diff --git a/lib/pages/new_private_chat/widget/expansion_contact_list_tile.dart b/lib/pages/new_private_chat/widget/expansion_contact_list_tile.dart index 54efe2d7d0..e855238afb 100644 --- a/lib/pages/new_private_chat/widget/expansion_contact_list_tile.dart +++ b/lib/pages/new_private_chat/widget/expansion_contact_list_tile.dart @@ -85,7 +85,8 @@ class ExpansionContactListTile extends StatelessWidget { ), ], const SizedBox(width: 8.0), - if (contact.status != null) + if (contact.status != null && + contact.status == ContactStatus.inactive) ContactStatusWidget( status: contact.status!, ), diff --git a/lib/pages/new_private_chat/widget/expansion_list.dart b/lib/pages/new_private_chat/widget/expansion_list.dart index c4ef272367..4120b62d8c 100644 --- a/lib/pages/new_private_chat/widget/expansion_list.dart +++ b/lib/pages/new_private_chat/widget/expansion_list.dart @@ -10,7 +10,6 @@ import 'package:fluffychat/presentation/model/contact/get_presentation_contacts_ import 'package:fluffychat/presentation/model/contact/presentation_contact.dart'; import 'package:fluffychat/presentation/model/contact/presentation_contact_success.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; -import 'package:fluffychat/widgets/contacts_warning_banner/contacts_warning_banner_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; @@ -164,7 +163,6 @@ class ExpansionList extends StatelessWidget { const SizedBox( height: 12, ), - _contactsWarningBannerViewBuilder(), ..._buildResponsiveButtons(context), for (final child in expansionList) ...[child], ] else ...[ @@ -188,15 +186,6 @@ class ExpansionList extends StatelessWidget { ); } - Widget _contactsWarningBannerViewBuilder() { - return ContactsWarningBannerView( - warningBannerNotifier: warningBannerNotifier, - isShowMargin: false, - closeContactsWarningBanner: closeContactsWarningBanner, - goToSettingsForPermissionActions: goToSettingsForPermissionActions, - ); - } - Widget _buildTitle(BuildContext context, int countContacts) { return Padding( padding: const EdgeInsets.only(left: 8.0), diff --git a/lib/pages/search/search_contacts_and_chats_controller.dart b/lib/pages/search/search_contacts_and_chats_controller.dart index 3b58fd1f41..a12e50664c 100644 --- a/lib/pages/search/search_contacts_and_chats_controller.dart +++ b/lib/pages/search/search_contacts_and_chats_controller.dart @@ -83,27 +83,8 @@ class SearchContactsAndChatsController with SearchDebouncerMixin, SearchMixin { .toList(); final tomContactPresentationSearchMatched = tomPresentationSearchContacts .expand((contact) => contact.toPresentationSearch()) - .where((contact) { - if (contact is! ContactPresentationSearch) { - return false; - } - - if (contact.displayName == null) { - return false; - } - - if (contact.email == null) { - return false; - } - - final matchedName = - contact.displayName!.toLowerCase().contains(keyword.toLowerCase()); - - final matchedEmail = - contact.email!.toLowerCase().contains(keyword.toLowerCase()); - - return matchedName || matchedEmail; - }).toList(); + .where((contact) => _doesMatchKeyword(contact, keyword)) + .toList(); _searchRecentChatInteractor .execute( keyword: keyword, @@ -126,6 +107,45 @@ class SearchContactsAndChatsController with SearchDebouncerMixin, SearchMixin { ); } + bool _matchedMatrixId(PresentationSearch contact, String keyword) { + return contact.directChatMatrixID + ?.toLowerCase() + .contains(keyword.toLowerCase()) ?? + false; + } + + bool _matchedName(PresentationSearch contact, String keyword) { + return contact.displayName?.toLowerCase().contains(keyword.toLowerCase()) ?? + false; + } + + bool _matchedEmail(PresentationSearch contact, String keyword) { + return contact.email?.toLowerCase().contains(keyword.toLowerCase()) ?? + false; + } + + bool _matchedContactInfo(PresentationSearch contact, String keyword) { + return _matchedName(contact, keyword) || + _matchedEmail(contact, keyword) || + _matchedMatrixId(contact, keyword); + } + + bool _doesMatchKeyword(PresentationSearch contact, String keyword) { + if (contact is! ContactPresentationSearch) { + return false; + } + + if (contact.displayName == null) { + return false; + } + + if (contact.email == null) { + return false; + } + + return _matchedContactInfo(contact, keyword); + } + void onSearchBarChanged(String keyword) { setDebouncerValue(keyword); } diff --git a/lib/pages/search/search_text_field.dart b/lib/pages/search/search_text_field.dart index 9c358316b1..408859555a 100644 --- a/lib/pages/search/search_text_field.dart +++ b/lib/pages/search/search_text_field.dart @@ -1,8 +1,10 @@ import 'package:fluffychat/pages/search/search_view_style.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:fluffychat/pages/dialer/pip/dismiss_keyboard.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; class SearchTextField extends StatelessWidget { final TextEditingController textEditingController; @@ -28,6 +30,7 @@ class SearchTextField extends StatelessWidget { }, controller: textEditingController, textInputAction: TextInputAction.search, + contextMenuBuilder: mobileTwakeContextMenuBuilder, enabled: true, focusNode: focusNode, autofocus: autofocus, @@ -44,7 +47,7 @@ class SearchTextField extends StatelessWidget { prefixIcon: Icon( Icons.search_outlined, size: SearchViewStyle.searchIconSize, - color: Theme.of(context).colorScheme.onSurface, + color: LinagoraRefColors.material().neutral[60], ), suffixIcon: ValueListenableBuilder( valueListenable: textEditingController, diff --git a/lib/pages/search/server_search_controller.dart b/lib/pages/search/server_search_controller.dart index 938426a5f3..db490e0134 100644 --- a/lib/pages/search/server_search_controller.dart +++ b/lib/pages/search/server_search_controller.dart @@ -89,7 +89,25 @@ class ServerSearchController with SearchDebouncerMixin { if (success is ServerSearchChatSuccess) { updateNextBatch(success.nextBatch); if (success.results?.isEmpty == true) { - searchResultsNotifier.value = PresentationServerSideEmptySearch(); + if (isLoadingMoreNotifier.value) { + searchResultsNotifier.value = PresentationServerSideSearch( + searchResults: [ + if (searchResultsNotifier.value + is PresentationServerSideSearch) + ...(searchResultsNotifier.value + as PresentationServerSideSearch) + .searchResults, + ...success.results ?? [], + ] + .where( + (result) => + result.isDisplayableResult(context: currentContext), + ) + .toList(), + ); + } else { + searchResultsNotifier.value = PresentationServerSideEmptySearch(); + } } else { searchResultsNotifier.value = PresentationServerSideSearch( searchResults: [ diff --git a/lib/pages/search/server_search_view.dart b/lib/pages/search/server_search_view.dart index 8be0ae054a..07a1512202 100644 --- a/lib/pages/search/server_search_view.dart +++ b/lib/pages/search/server_search_view.dart @@ -89,7 +89,7 @@ class ServerSearchMessagesList extends StatelessWidget { maxLines: 2, style: ChatLitSubSubtitleTextStyleView .textStyle - .textStyle(room), + .textStyle(room, context), ), ], ), diff --git a/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart b/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart index 26929dfc54..6abdcf5658 100644 --- a/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart +++ b/lib/pages/settings_dashboard/settings_emotes/settings_emotes_view.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/pages/settings_dashboard/settings/settings_app_bar.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -52,6 +53,7 @@ class EmotesSettingsView extends StatelessWidget { ), child: TextField( controller: controller.newImageCodeController, + contextMenuBuilder: mobileTwakeContextMenuBuilder, autocorrect: false, minLines: 1, maxLines: 1, @@ -158,6 +160,8 @@ class EmotesSettingsView extends StatelessWidget { child: TextField( readOnly: controller.readonly, controller: textEditingController, + contextMenuBuilder: + mobileTwakeContextMenuBuilder, autocorrect: false, minLines: 1, maxLines: 1, diff --git a/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list_view.dart b/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list_view.dart index c86b8da69d..7e83739f90 100644 --- a/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list_view.dart +++ b/lib/pages/settings_dashboard/settings_ignore_list/settings_ignore_list_view.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/pages/settings_dashboard/settings/settings_app_bar.dart'; import 'package:fluffychat/utils/dialog/twake_dialog.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -36,6 +37,7 @@ class SettingsIgnoreListView extends StatelessWidget { children: [ TextField( controller: controller.controller, + contextMenuBuilder: mobileTwakeContextMenuBuilder, autocorrect: false, textInputAction: TextInputAction.done, onSubmitted: (_) => controller.ignoreUser(context), diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index 63bdb72b24..8fb0d4bc22 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -208,7 +208,7 @@ class SettingsProfileController extends State _getImageOnWeb(context); return; } - final currentPermissionPhotos = await getCurrentMediaPermission(); + final currentPermissionPhotos = await getCurrentMediaPermission(context); if (currentPermissionPhotos != null) { final imagePickerController = createImagePickerController(); showImagePickerBottomSheet( diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart index a3054c9c05..53235e8ce4 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_item.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_item_style.dart'; import 'package:fluffychat/presentation/enum/settings/settings_profile_enum.dart'; import 'package:fluffychat/presentation/model/settings/settings_profile_presentation.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:flutter/material.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; @@ -70,6 +71,7 @@ class SettingsProfileItemBuilder extends StatelessWidget { onChange!(value, settingsProfileEnum), readOnly: !settingsProfilePresentation.isEditable, autofocus: false, + contextMenuBuilder: mobileTwakeContextMenuBuilder, focusNode: focusNode, controller: textEditingController, decoration: InputDecoration( diff --git a/lib/pages/share/share.dart b/lib/pages/share/share.dart index 1072679aae..1c3a40a461 100644 --- a/lib/pages/share/share.dart +++ b/lib/pages/share/share.dart @@ -74,7 +74,8 @@ class ShareController extends State ); final shareContentList = Matrix.of(context).shareContentList; final shareContent = Matrix.of(context).shareContent; - + Logs().d('ShareController::shareTo() shareContent: $shareContent'); + Logs().d('ShareController::shareTo() shareContentList: $shareContentList'); if (shareContentList.isNotEmpty) { _handleShareFilesContent( room: room, @@ -93,6 +94,7 @@ class ShareController extends State Map? textContent, }) { if (textContent == null) return; + Navigator.pop(context); room.sendEvent(textContent); context.go('/rooms/${room.id}'); } @@ -107,6 +109,7 @@ class ShareController extends State content?.tryGet('msgtype') == TwakeEventTypes.shareFileEventType, )) { + Navigator.pop(context); context.go( '/rooms/${room.id}', extra: ChatRouterInputArgument( diff --git a/lib/pages/story/story_view.dart b/lib/pages/story/story_view.dart index 131ff35faa..3a2dcc0b9e 100644 --- a/lib/pages/story/story_view.dart +++ b/lib/pages/story/story_view.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -355,6 +356,7 @@ class StoryView extends StatelessWidget { onSubmitted: controller.replyAction, textInputAction: TextInputAction.send, readOnly: controller.replyLoading, + contextMenuBuilder: mobileTwakeContextMenuBuilder, decoration: InputDecoration( contentPadding: const EdgeInsets.fromLTRB(0, 16, 0, 16), diff --git a/lib/presentation/decorators/chat_list/subtitle_image_preview_style.dart b/lib/presentation/decorators/chat_list/subtitle_image_preview_style.dart new file mode 100644 index 0000000000..b63c4b30bc --- /dev/null +++ b/lib/presentation/decorators/chat_list/subtitle_image_preview_style.dart @@ -0,0 +1,9 @@ +import 'package:flutter/material.dart'; + +class SubtitleImagePreviewStyle { + static const double width = 20; + static const double height = 20; + static const double borderRadius = 4; + static const BoxFit fit = BoxFit.fill; + static const EdgeInsets labelPadding = EdgeInsets.only(left: 5); +} diff --git a/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_component.dart b/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_component.dart index ba8b925fcd..f2cddf4801 100644 --- a/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_component.dart +++ b/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_component.dart @@ -2,5 +2,5 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; abstract class ChatListSubtitleTextStyleComponent { - TextStyle textStyle(Room room); + TextStyle textStyle(Room room, BuildContext context); } diff --git a/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_decorator.dart b/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_decorator.dart index c8ffb4fd67..7e1d03c7b0 100644 --- a/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_decorator.dart +++ b/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_decorator.dart @@ -18,8 +18,8 @@ class ChatListSubtitleTextStyle implements ChatListSubtitleTextStyleDecorator { ChatListSubtitleTextStyle(this._interfaceTextStyleComponent); @override - TextStyle textStyle(Room room) { - return _interfaceTextStyleComponent.textStyle(room); + TextStyle textStyle(Room room, BuildContext context) { + return _interfaceTextStyleComponent.textStyle(room, context); } @override @@ -30,8 +30,8 @@ class ChatListSubtitleTextStyle implements ChatListSubtitleTextStyleDecorator { class ReadChatListSubtitleTextStyleDecorator implements ChatListSubtitleTextStyleComponent { @override - TextStyle textStyle(Room room) { - return LinagoraTextStyle.material().bodyMedium3.copyWith( + TextStyle textStyle(Room room, BuildContext context) { + return Theme.of(context).textTheme.bodyMedium!.copyWith( color: LinagoraSysColors.material().onSurface, fontFamily: GoogleFonts.inter().fontFamily, ); @@ -45,15 +45,15 @@ class UnreadChatListSubtitleTextStyleDecorator UnreadChatListSubtitleTextStyleDecorator(this._interfaceTextStyleComponent); @override - TextStyle textStyle(Room room) { + TextStyle textStyle(Room room, BuildContext context) { if (room.isUnreadOrInvited) { - return _interfaceTextStyleComponent.textStyle(room).merge( - LinagoraTextStyle.material().bodyMedium2.copyWith( + return _interfaceTextStyleComponent.textStyle(room, context).merge( + Theme.of(context).textTheme.bodyMedium!.copyWith( color: LinagoraSysColors.material().onSurface, ), ); } else { - return _interfaceTextStyleComponent.textStyle(room); + return _interfaceTextStyleComponent.textStyle(room, context); } } @@ -69,13 +69,13 @@ class MuteChatListSubtitleTextStyleDecorator MuteChatListSubtitleTextStyleDecorator(this._interfaceTextStyleComponent); @override - TextStyle textStyle(Room room) { + TextStyle textStyle(Room room, BuildContext context) { if (room.isMuted) { - return _interfaceTextStyleComponent.textStyle(room).copyWith( - color: LinagoraRefColors.material().tertiary[20], + return _interfaceTextStyleComponent.textStyle(room, context).copyWith( + color: LinagoraSysColors.material().onSurface, ); } else { - return _interfaceTextStyleComponent.textStyle(room); + return _interfaceTextStyleComponent.textStyle(room, context); } } diff --git a/lib/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_decorator.dart b/lib/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_decorator.dart index bb25c10c6b..15435932f2 100644 --- a/lib/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_decorator.dart +++ b/lib/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_decorator.dart @@ -30,7 +30,7 @@ class ReadChatListTitleTextStyleDecorator implements ChatListTitleTextStyleComponent { @override TextStyle textStyle(Room room) { - return LinagoraTextStyle.material().bodyLarge2.copyWith( + return LinagoraTextStyle.material().bodyMedium2.copyWith( color: LinagoraSysColors.material().onSurface, fontFamily: GoogleFonts.inter().fontFamily, ); @@ -47,8 +47,9 @@ class UnreadChatListTitleTextStyleDecorator TextStyle textStyle(Room room) { if (room.isUnreadOrInvited) { return _interfaceTextStyleComponent.textStyle(room).merge( - LinagoraTextStyle.material().bodyLarge1.copyWith( + LinagoraTextStyle.material().bodyMedium2.copyWith( color: LinagoraSysColors.material().onSurface, + fontFamily: GoogleFonts.inter().fontFamily, ), ); } else { @@ -72,7 +73,7 @@ class MuteChatListTitleTextStyleDecorator final isMuted = room.pushRuleState != PushRuleState.notify; if (isMuted) { return _interfaceTextStyleComponent.textStyle(room).copyWith( - color: LinagoraRefColors.material().tertiary[20], + color: LinagoraSysColors.material().onSurface, ); } else { return _interfaceTextStyleComponent.textStyle(room); diff --git a/lib/presentation/extensions/send_file_extension.dart b/lib/presentation/extensions/send_file_extension.dart index 5cb216ed3c..4042f80559 100644 --- a/lib/presentation/extensions/send_file_extension.dart +++ b/lib/presentation/extensions/send_file_extension.dart @@ -20,6 +20,7 @@ import 'package:fluffychat/utils/extension/mime_type_extension.dart'; import 'package:fluffychat/utils/manager/storage_directory_manager.dart'; import 'package:fluffychat/utils/manager/upload_manager/upload_state.dart'; import 'package:flutter/widgets.dart'; +import 'package:heif_converter/heif_converter.dart'; import 'package:image/image.dart' as img; import 'package:blurhash_dart/blurhash_dart.dart'; import 'package:flutter/foundation.dart'; @@ -49,7 +50,6 @@ extension SendFileExtension on Room { CancelToken? cancelToken, DateTime? sentDate, }) async { - FileInfo tempfileInfo = fileInfo; // Check media config of the server before sending the file. Stop if the // Media config is unreachable or the file is bigger than the given maxsize. try { @@ -84,17 +84,24 @@ extension SendFileExtension on Room { if (TwakeMimeTypeExtension.heicMimeTypes.contains(fileInfo.mimeType) && fileInfo is ImageFileInfo) { + try { + final oldFilePath = fileInfo.filePath; + fileInfo = await convertHeicToJpgImage(fileInfo); + File(oldFilePath).delete(); + } catch (e) { + Logs().e('sendFileEvent::Error while converting heic to jpg', e); + } + final formattedDateTime = DateTime.now().getFormattedCurrentDateTime(); - final targetPath = - await File('${tempDir.path}/$formattedDateTime${fileInfo.fileName}') - .create(); + final fileName = _generateThumbnailFileName(formattedDateTime, fileInfo); + final targetPath = await _createThumbnailFile(tempDir, fileName); await _generateThumbnail( - fileInfo, + fileInfo as ImageFileInfo, targetPath: targetPath.path, uploadStreamController: uploadStreamController, ); - fileInfo = ImageFileInfo( - fileInfo.fileName, + thumbnail = ImageFileInfo( + fileName, targetPath.path, await targetPath.length(), width: fileInfo.width, @@ -165,7 +172,10 @@ extension SendFileExtension on Room { txid, uploadStreamController: uploadStreamController, ); - if (fileInfo.width == null || fileInfo.height == null) { + if (fileInfo.width == null || + fileInfo.height == null || + fileInfo.width == 0 || + fileInfo.height == 0) { fileInfo = VideoFileInfo( fileInfo.fileName, fileInfo.filePath, @@ -224,7 +234,7 @@ extension SendFileExtension on Room { ); } - tempfileInfo = FileInfo( + fileInfo = FileInfo( fileInfo.fileName, tempEncryptedFile.path, fileInfo.fileSize, @@ -268,7 +278,7 @@ extension SendFileExtension on Room { try { final mediaApi = getIt.get(); final response = await mediaApi.uploadFileMobile( - fileInfo: tempfileInfo, + fileInfo: fileInfo, cancelToken: cancelToken, onSendProgress: (receive, total) { if (uploadStreamController?.isClosed == true) return; @@ -418,6 +428,44 @@ extension SendFileExtension on Room { return eventId; } + Future _createThumbnailFile(Directory tempDir, String fileName) async => + await File('${tempDir.path}/$fileName').create(); + + String _generateThumbnailFileName( + String formattedDateTime, + FileInfo fileInfo, + ) => + '$formattedDateTime${fileInfo.fileName}.${AppConfig.imageCompressFormmat.name}'; + + Future convertHeicToJpgImage(ImageFileInfo fileInfo) async { + final convertedFilePath = + StorageDirectoryManager.instance.convertFileExtension( + fileInfo.filePath, + 'jpg', + ); + final newPath = await HeifConverter.convert( + fileInfo.filePath, + output: convertedFilePath, + ); + Logs().d('sendFileEvent::Heic converted to jpg', newPath); + if (newPath != null) { + final newConvertedFile = File(convertedFilePath); + fileInfo = ImageFileInfo( + newConvertedFile.path.split("/").last, + newConvertedFile.path, + await newConvertedFile.length(), + width: fileInfo.width, + height: fileInfo.height, + ); + } else { + Logs().e( + 'sendFileEvent::Error while converting heic to jpg:newPath is null', + ); + throw Exception('sendFileEvent::Error while converting heic to jpg'); + } + return fileInfo; + } + Future _copyFileInMemToAppDownloadsFolder({ required String sendingEventId, required String eventId, @@ -575,13 +623,13 @@ extension SendFileExtension on Room { originalFile.filePath, targetPath, quality: AppConfig.thumbnailQuality, - format: CompressFormat.jpeg, + format: AppConfig.imageCompressFormmat, ); if (result == null) return null; final size = await result.length(); var width = originalFile.width; var height = originalFile.height; - if (width == null || height == null) { + if (width == null || height == null || width == 0 || height == 0) { final imageDimension = await runBenchmarked( '_calculateImageDimension', () => _calculateImageDimension(result.path), diff --git a/lib/presentation/extensions/send_file_web_extension.dart b/lib/presentation/extensions/send_file_web_extension.dart index 8ce51baa67..42f08ff02a 100644 --- a/lib/presentation/extensions/send_file_web_extension.dart +++ b/lib/presentation/extensions/send_file_web_extension.dart @@ -19,6 +19,7 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dar import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:matrix/matrix.dart'; import 'package:image/image.dart'; +import 'package:mime/mime.dart'; import 'package:video_player/video_player.dart'; import 'package:video_thumbnail/video_thumbnail.dart'; @@ -169,7 +170,7 @@ extension SendFileWebExtension on Room { : null; if (uploadThumbnail != null && uploadThumbnail.bytes != null) { final uploadThumbnailResponse = await mediaApi.uploadFileWeb( - file: file, + file: uploadThumbnail, cancelToken: cancelToken, onSendProgress: (receive, total) { uploadStreamController?.add( @@ -372,6 +373,7 @@ extension SendFileWebExtension on Room { final result = await FlutterImageCompress.compressWithList( originalFile.bytes!, quality: AppConfig.thumbnailQuality, + format: AppConfig.imageCompressFormmat, ); final blurHash = await runBenchmarked( @@ -385,7 +387,7 @@ extension SendFileWebExtension on Room { return MatrixImageFile( bytes: result, - name: originalFile.name, + name: '${originalFile.name}.${AppConfig.imageCompressFormmat.name}', mimeType: originalFile.mimeType, width: originalFile.width, height: originalFile.height, @@ -425,9 +427,10 @@ extension SendFileWebExtension on Room { ); throw exception; } + final result = await VideoThumbnail.thumbnailData( video: url, - imageFormat: ImageFormat.JPEG, + imageFormat: AppConfig.videoThumbnailFormat, quality: AppConfig.thumbnailQuality, ); final thumbnailBitmap = await convertUint8ListToBitmap(result); @@ -440,10 +443,12 @@ extension SendFileWebExtension on Room { const Right(GenerateThumbnailSuccess()), ); + final thumbnailFileName = _getVideoThumbnailFileName(originalFile); + return MatrixImageFile( bytes: result, - name: originalFile.name, - mimeType: originalFile.mimeType, + name: thumbnailFileName, + mimeType: lookupMimeType(thumbnailFileName) ?? 'image/jpeg', width: thumbnailBitmap?.width, height: thumbnailBitmap?.height, blurhash: blurHash, @@ -461,6 +466,9 @@ extension SendFileWebExtension on Room { } } + String _getVideoThumbnailFileName(MatrixVideoFile originalFile) => + '${originalFile.name}.${AppConfig.videoThumbnailFormat.name.toLowerCase()}'; + Future _getVideoDuration( MatrixVideoFile originalFile, ) async { diff --git a/lib/presentation/mixins/chat_list_item_mixin.dart b/lib/presentation/mixins/chat_list_item_mixin.dart index ca1bc8def5..1b2edf55ed 100644 --- a/lib/presentation/mixins/chat_list_item_mixin.dart +++ b/lib/presentation/mixins/chat_list_item_mixin.dart @@ -1,8 +1,14 @@ +import 'package:fluffychat/pages/chat/events/images_builder/image_placeholder.dart'; +import 'package:fluffychat/presentation/decorators/chat_list/subtitle_image_preview_style.dart'; import 'package:fluffychat/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_view.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/mxc_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; +import 'package:linagora_design_flutter/style/linagora_text_style.dart'; import 'package:matrix/matrix.dart'; mixin ChatListItemMixin { @@ -38,20 +44,22 @@ mixin ChatListItemMixin { softWrap: false, maxLines: isGroup ? 1 : 2, overflow: TextOverflow.ellipsis, - style: ChatLitSubSubtitleTextStyleView.textStyle.textStyle(room), + style: LinagoraTextStyle.material().bodyMedium3.copyWith( + color: LinagoraRefColors.material().tertiary[30], + ), ); }, ); } Widget typingTextWidget(String typingText, BuildContext context) { - final displayedTypingText = "~ $typingText…"; + final displayedTypingText = "$typingText…"; return Text( displayedTypingText, - style: Theme.of(context).textTheme.labelLarge?.merge( + style: LinagoraTextStyle.material().bodyMedium2.merge( TextStyle( overflow: TextOverflow.ellipsis, - color: LinagoraRefColors.material().secondary, + color: LinagoraRefColors.material().tertiary[30], ), ), maxLines: 2, @@ -74,7 +82,7 @@ mixin ChatListItemMixin { maxLines: 1, softWrap: false, style: ChatLitSubSubtitleTextStyleView.textStyle - .textStyle(room), + .textStyle(room, context), ); }, ), @@ -95,7 +103,8 @@ mixin ChatListItemMixin { softWrap: false, maxLines: 2, overflow: TextOverflow.ellipsis, - style: ChatLitSubSubtitleTextStyleView.textStyle.textStyle(room), + style: + ChatLitSubSubtitleTextStyleView.textStyle.textStyle(room, context), ); } @@ -112,15 +121,93 @@ mixin ChatListItemMixin { removeBreakLine: true, ) ?? L10n.of(context)!.emptyChat; + if (room.lastEvent?.isAFile == true) { + return Text( + "${snapshot.data!.calcDisplayname()}: ${room.lastEvent?.filename ?? subscriptions}", + softWrap: false, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: ChatLitSubSubtitleTextStyleView.textStyle + .textStyle(room, context), + ); + } - return Text( - "${snapshot.data!.calcDisplayname()}: $subscriptions", - softWrap: false, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: ChatLitSubSubtitleTextStyleView.textStyle.textStyle(room), + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + snapshot.data!.calcDisplayname(), + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: LinagoraSysColors.material().onSurface, + ), + ), + room.lastEvent?.messageType == MessageTypes.Image || + room.lastEvent?.messageType == MessageTypes.Video + ? chatlistItemMediaPreviewSubTitle( + context, + room, + ) + : Text( + subscriptions, + softWrap: false, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: LinagoraTextStyle.material().bodyMedium3.copyWith( + color: LinagoraRefColors.material().tertiary[30], + ), + ), + ], ); }, ); } + + Widget chatlistItemMediaPreviewSubTitle( + BuildContext context, + Room room, + ) { + return Row( + children: [ + if (room.lastEvent?.status != EventStatus.synced) + const SizedBox.shrink() + else + SizedBox( + height: SubtitleImagePreviewStyle.height, + width: SubtitleImagePreviewStyle.width, + child: ClipRRect( + borderRadius: + BorderRadius.circular(SubtitleImagePreviewStyle.borderRadius), + child: MxcImage( + key: ValueKey(room.lastEvent!.eventId), + cacheKey: room.lastEvent!.eventId, + event: room.lastEvent!, + placeholder: (context) => ImagePlaceholder( + event: room.lastEvent!, + width: SubtitleImagePreviewStyle.width, + height: SubtitleImagePreviewStyle.height, + fit: SubtitleImagePreviewStyle.fit, + ), + fit: SubtitleImagePreviewStyle.fit, + enableHeroAnimation: false, + ), + ), + ), + Padding( + padding: SubtitleImagePreviewStyle.labelPadding, + child: Text( + room.lastEvent!.messageType == MessageTypes.Image + ? L10n.of(context)!.photo + : L10n.of(context)!.video, + style: LinagoraTextStyle.material() + .bodyMedium3 + .copyWith(color: LinagoraRefColors.material().tertiary[30]), + ), + ), + ], + ); + } } diff --git a/lib/presentation/mixins/common_media_picker_mixin.dart b/lib/presentation/mixins/common_media_picker_mixin.dart index e856d93ec3..283f74746b 100644 --- a/lib/presentation/mixins/common_media_picker_mixin.dart +++ b/lib/presentation/mixins/common_media_picker_mixin.dart @@ -1,3 +1,5 @@ +import 'package:fluffychat/config/localizations/localization_service.dart'; +import 'package:fluffychat/utils/localized_camera_picker_text_delegate.dart'; import 'package:fluffychat/utils/permission_dialog.dart'; import 'package:fluffychat/utils/permission_service.dart'; import 'package:flutter/material.dart'; @@ -14,8 +16,8 @@ mixin CommonMediaPickerMixin { final PermissionHandlerService _permissionHandlerService = PermissionHandlerService(); - Future? getCurrentMediaPermission() { - return _permissionHandlerService.requestPermissionForMediaActions(); + Future? getCurrentMediaPermission(BuildContext context) { + return _permissionHandlerService.requestPermissionForMediaActions(context); } Future? getCurrentCameraPermission() { @@ -23,7 +25,7 @@ mixin CommonMediaPickerMixin { } Future? getCurrentMicroPermission() { - return _permissionHandlerService.requestPermissionForMircoActions(); + return _permissionHandlerService.requestPermissionForMicroActions(); } void goToSettings( @@ -40,11 +42,11 @@ mixin CommonMediaPickerMixin { text: isMicrophone ? L10n.of(context)!.tapToAllowAccessToYourMicrophone : L10n.of(context)!.tapToAllowAccessToYourCamera, - style: Theme.of(context).textTheme.titleSmall, + style: Theme.of(context).textTheme.bodyMedium, children: [ TextSpan( text: ' ${L10n.of(context)!.twake}.', - style: Theme.of(context).textTheme.titleSmall!.copyWith( + style: Theme.of(context).textTheme.bodyMedium!.copyWith( fontWeight: FontWeight.bold, ), ), @@ -80,14 +82,36 @@ mixin CommonMediaPickerMixin { context, pickerConfig: onlyImage ? CameraPickerConfig( + textDelegate: getTextDelegateForLocale( + context, + ), enableAudio: false, onError: (e, a) => _onError(context: context, error: e), ) : CameraPickerConfig( + textDelegate: getTextDelegateForLocale( + context, + ), enableRecording: true, onError: (e, a) => _onError(context: context, error: e), ), - locale: View.of(context).platformDispatcher.locale, ); } + + CameraPickerTextDelegate getTextDelegateForLocale( + BuildContext context, + ) { + switch (LocalizationService.currentLocale.value.languageCode) { + case 'ru': + case 'fr': + return LocalizedCameraPickerTextDelegate( + context, + LocalizationService.currentLocale.value.languageCode, + ); + default: + return cameraPickerTextDelegateFromLocale( + LocalizationService.currentLocale.value, + ); + } + } } diff --git a/lib/presentation/mixins/contacts_view_controller_mixin.dart b/lib/presentation/mixins/contacts_view_controller_mixin.dart index 983192ba5f..224471545c 100644 --- a/lib/presentation/mixins/contacts_view_controller_mixin.dart +++ b/lib/presentation/mixins/contacts_view_controller_mixin.dart @@ -20,9 +20,11 @@ import 'package:fluffychat/presentation/model/contact/presentation_contact.dart' import 'package:fluffychat/presentation/model/contact/presentation_contact_success.dart'; import 'package:fluffychat/presentation/model/search/presentation_search.dart'; import 'package:fluffychat/presentation/model/search/presentation_search_state_extension.dart'; +import 'package:fluffychat/utils/permission_dialog.dart'; import 'package:fluffychat/utils/permission_service.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -67,15 +69,110 @@ mixin class ContactsViewControllerMixin { final contactsManager = getIt.get(); - PermissionStatus contactsPermissionStatus = PermissionStatus.granted; + PermissionStatus? contactsPermissionStatus; + + Future displayContactPermissionDialog(BuildContext context) async { + final fetchContactsPermissionStatus = + await _permissionHandlerService.contactsPermissionStatus; + + contactsPermissionStatus = fetchContactsPermissionStatus; + + if (PlatformInfos.isMobile && !fetchContactsPermissionStatus.isGranted) { + await showDialog( + useRootNavigator: false, + context: context, + builder: (dialogContext) { + return PermissionDialog( + icon: const Icon(Icons.contact_page_outlined), + permission: Permission.contacts, + explainTextRequestPermission: Text( + L10n.of(context)!.explainPermissionToAccessContacts, + style: Theme.of(context).textTheme.bodyMedium, + ), + onRefuseTap: _handleDenyPermissionDialog, + onAcceptButton: () async { + Navigator.of(dialogContext).pop(); + await _handleRequestContactsPermission(); + }, + ); + }, + ); + } + } + + void _handleDenyPermissionDialog() { + warningBannerNotifier.value = WarningContactsBannerState.display; + contactsManager.updateNotShowWarningContactsDialogAgain = true; + } + + Future _initWarningBanner() async { + if (!PlatformInfos.isMobile) { + return; + } + final currentContactPermission = + await _permissionHandlerService.contactsPermissionStatus; + Logs().i( + 'ContactsViewControllerMixin::_initWarningBanner: Contact Permission $currentContactPermission', + ); + + if (currentContactPermission.isGranted) { + contactsPermissionStatus = currentContactPermission; + warningBannerNotifier.value = WarningContactsBannerState.hide; + return; + } + + if (!contactsManager.isDoNotShowWarningContactsBannerAgain && + contactsManager.isDoNotShowWarningContactsDialogAgain) { + warningBannerNotifier.value = WarningContactsBannerState.display; + return; + } + } + + Future handleDidChangeAppLifecycleState(AppLifecycleState state) async { + if (!PlatformInfos.isMobile) { + return; + } + Logs().i( + 'ContactsViewControllerMixin::handleDidChangeAppLifecycleState: $state', + ); + + if (state == AppLifecycleState.resumed) { + final currentContactPermission = + await _permissionHandlerService.contactsPermissionStatus; + + Logs().i( + 'ContactsViewControllerMixin::handleDidChangeAppLifecycleState: Contact Permission $currentContactPermission', + ); + + if (currentContactPermission != contactsPermissionStatus && + currentContactPermission.isDenied) { + if (!contactsManager.isDoNotShowWarningContactsBannerAgain) { + warningBannerNotifier.value = WarningContactsBannerState.display; + } + contactsPermissionStatus = currentContactPermission; + return; + } + + if (currentContactPermission != contactsPermissionStatus && + currentContactPermission.isGranted) { + contactsPermissionStatus = currentContactPermission; + warningBannerNotifier.value = WarningContactsBannerState.hide; + contactsManager.refreshPhonebookContacts(); + return; + } + } + } void initialFetchContacts({ + required BuildContext context, required Client client, required MatrixLocalizations matrixLocalizations, }) async { if (PlatformInfos.isMobile && - !contactsManager.isDoNotShowWarningContactsBannerAgain) { - await _handleRequestContactsPermission(); + !contactsManager.isDoNotShowWarningContactsDialogAgain) { + await displayContactPermissionDialog(context); + } else { + await _initWarningBanner(); } _refreshAllContacts( client: client, @@ -97,6 +194,7 @@ mixin class ContactsViewControllerMixin { }); contactsManager.initialSynchronizeContacts( isAvailableSupportPhonebookContacts: PlatformInfos.isMobile && + contactsPermissionStatus != null && contactsPermissionStatus == PermissionStatus.granted, ); } @@ -139,17 +237,23 @@ mixin class ContactsViewControllerMixin { contactsManager.getContactsNotifier().value.fold( (failure) { if (failure is GetContactsFailure) { - return Left( - GetPresentationContactsFailure( - keyword: keyword, + return _handleSearchExternalContact( + keyword, + otherResult: Left( + GetPresentationContactsFailure( + keyword: keyword, + ), ), ); } if (failure is GetContactsIsEmpty) { - return Left( - GetPresentationContactsEmpty( - keyword: keyword, + return _handleSearchExternalContact( + keyword, + otherResult: Left( + GetPresentationContactsEmpty( + keyword: keyword, + ), ), ); } @@ -199,17 +303,23 @@ mixin class ContactsViewControllerMixin { contactsManager.getPhonebookContactsNotifier().value.fold( (failure) { if (failure is GetPhonebookContactsFailure) { - return Left( - GetPresentationContactsFailure( - keyword: keyword, + return _handleSearchExternalContact( + keyword, + otherResult: Left( + GetPresentationContactsFailure( + keyword: keyword, + ), ), ); } if (failure is GetPhonebookContactsIsEmpty) { - return Left( - GetPresentationContactsEmpty( - keyword: keyword, + return _handleSearchExternalContact( + keyword, + otherResult: Left( + GetPresentationContactsEmpty( + keyword: keyword, + ), ), ); } @@ -241,6 +351,25 @@ mixin class ContactsViewControllerMixin { ); } + Either _handleSearchExternalContact( + String keyword, { + required Either otherResult, + }) { + if (keyword.isValidMatrixId && keyword.startsWith("@")) { + return Right( + PresentationExternalContactSuccess( + contact: PresentationContact( + matrixId: keyword, + displayName: keyword.substring(1), + type: ContactType.external, + ), + ), + ); + } else { + return otherResult; + } + } + Future _refreshRecentContacts({ required Client client, required MatrixLocalizations matrixLocalizations, @@ -296,8 +425,11 @@ mixin class ContactsViewControllerMixin { final currentContactsPermissionStatus = await _permissionHandlerService.requestContactsPermissionActions(); if (currentContactsPermissionStatus == PermissionStatus.granted) { + contactsManager.refreshPhonebookContacts(); warningBannerNotifier.value = WarningContactsBannerState.hide; } else { + contactsManager.updateNotShowWarningContactsDialogAgain = true; + if (!contactsManager.isDoNotShowWarningContactsBannerAgain) { warningBannerNotifier.value = WarningContactsBannerState.display; } diff --git a/lib/presentation/mixins/media_picker_mixin.dart b/lib/presentation/mixins/media_picker_mixin.dart index 4877314899..6854bc98c5 100644 --- a/lib/presentation/mixins/media_picker_mixin.dart +++ b/lib/presentation/mixins/media_picker_mixin.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/pages/chat/item_actions_bottom_widget.dart'; import 'package:fluffychat/pages/chat/send_file_dialog/send_file_dialog_style.dart'; import 'package:fluffychat/presentation/style/media_picker_style.dart'; import 'package:fluffychat/resource/image_paths.dart'; +import 'package:fluffychat/utils/permission_service.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -33,6 +34,9 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { // PickerType.contact, ]; + final PermissionHandlerService _permissionHandlerService = + PermissionHandlerService(); + void showMediaPickerBottomSheetAction({ required BuildContext context, required ImagePickerGridController imagePickerGridController, @@ -44,21 +48,26 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { TextEditingController? captionController, ValueKey? typeAheadKey, }) async { - final currentPermissionPhotos = await getCurrentMediaPermission(); - if (currentPermissionPhotos != null) { - showMediasPickerBottomSheet( - context: context, - imagePickerController: imagePickerGridController, - permissionStatusPhotos: currentPermissionPhotos, - onSendTap: onSendTap, - room: room, - onPickerTypeTap: onPickerTypeTap, - onCameraPicked: onCameraPicked, - focusSuggestionController: focusSuggestionController, - captionController: captionController, - typeAheadKey: typeAheadKey, + await getCurrentMediaPermission(context)?.then((currentPermissionPhotos) { + if (currentPermissionPhotos != null) { + showMediasPickerBottomSheet( + context: context, + imagePickerController: imagePickerGridController, + permissionStatusPhotos: currentPermissionPhotos, + onSendTap: onSendTap, + room: room, + onPickerTypeTap: onPickerTypeTap, + onCameraPicked: onCameraPicked, + focusSuggestionController: focusSuggestionController, + captionController: captionController, + typeAheadKey: typeAheadKey, + ); + } + }).onError((error, _) { + Logs().e( + "MediaPickerMixin::showMediaPickerBottomSheetAction(): error - $error", ); - } + }); } Future showMediasPickerBottomSheet({ @@ -288,6 +297,11 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { ), ), ), + onGoToSettings: (context) async { + Navigator.pop(context); + await _permissionHandlerService + .requestPermissionForMediaActions(context); + }, goToSettingsWidget: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -346,12 +360,21 @@ mixin MediaPickerMixin on CommonMediaPickerMixin { OnCameraPicked? onCameraPicked, bool onlyImage = false, }) async { - final assetEntity = + var assetEntity = await pickMediaFromCameraAction(context: context, onlyImage: onlyImage); Logs().d( "MediaPickerMixin::_pickFromCameraAction(): assetEntity - $assetEntity", ); if (assetEntity != null) { + // TODO: TW-1844: Remove this when the issue https://github.com/fluttercandies/flutter_wechat_camera_picker/issues/266 + if (PlatformInfos.isAndroid) { + assetEntity = AssetEntity( + id: assetEntity.id, + width: 0, + height: 0, + typeInt: assetEntity.typeInt, + ); + } imagePickerGridController.pickAssetFromCamera(assetEntity); if (onCameraPicked != null) { diff --git a/lib/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart b/lib/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart index e2b5cd7299..0d8d310abc 100644 --- a/lib/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart +++ b/lib/presentation/mixins/save_file_to_twake_downloads_folder_mixin.dart @@ -244,6 +244,7 @@ mixin SaveFileToTwakeAndroidDownloadsFolderMixin { L10n.of(context)!.explainPermissionToDownloadFiles( AppConfig.applicationName, ), + style: Theme.of(context).textTheme.bodyMedium, ), onAcceptButton: () => PermissionHandlerService().goToSettingsForPermissionActions(), diff --git a/lib/presentation/mixins/save_media_to_gallery_android_mixin.dart b/lib/presentation/mixins/save_media_to_gallery_android_mixin.dart index cee82bc2e5..4e8405688b 100644 --- a/lib/presentation/mixins/save_media_to_gallery_android_mixin.dart +++ b/lib/presentation/mixins/save_media_to_gallery_android_mixin.dart @@ -137,6 +137,7 @@ mixin SaveMediaToGalleryAndroidMixin L10n.of(context)!.explainPermissionToGallery( AppConfig.applicationName, ), + style: Theme.of(context).textTheme.bodyMedium, ), onAcceptButton: () => permissionHandlerService.goToSettingsForPermissionActions(), diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index e91a0e454c..1387ba1631 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -27,6 +27,8 @@ import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/presentation/extensions/go_router_extensions.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; import 'package:fluffychat/utils/push_helper.dart'; +import 'package:fluffychat/widgets/layouts/agruments/receive_content_args.dart'; +import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; import 'package:fluffychat/widgets/twake_app.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -387,7 +389,7 @@ class BackgroundPush { }) async { try { Logs().v('[Push] Attempting to go to room $roomId...'); - _clearAllNavigatorAvailable(roomId: roomId); + await _clearAllNavigatorAvailable(roomId: roomId); if (_matrixState == null || roomId == null) { return; } @@ -626,9 +628,27 @@ class BackgroundPush { ); } - void _clearAllNavigatorAvailable({ + Future _handleInnerNavigation() async { + if (TwakeApp.isCurrentPageIsNotRooms()) { + return; + } + + if (TwakeApp.isCurrentPageIsInRooms()) { + Logs().d("BackgroundPush::_handleInnerNavigation(): CurrentRoomActive"); + TwakeApp.router.go( + '/rooms', + extra: ReceiveContentArgs( + newActiveClient: client, + activeDestination: AdaptiveDestinationEnum.rooms, + ), + ); + await Future.delayed(const Duration(milliseconds: 500)); + } + } + + Future _clearAllNavigatorAvailable({ String? roomId, - }) { + }) async { Logs().d( "BackgroundPush:: - Current active room id ${TwakeApp.router.activeRoomId}", ); @@ -636,11 +656,7 @@ class BackgroundPush { return; } - final canPopNavigation = TwakeApp.router.routerDelegate.canPop(); - Logs().d("BackgroundPush:: - Can pop other Navigation $canPopNavigation"); - if (canPopNavigation) { - TwakeApp.router.routerDelegate.pop(); - } + await _handleInnerNavigation(); } void _handleRedirectRoom( diff --git a/lib/utils/localized_camera_picker_text_delegate.dart b/lib/utils/localized_camera_picker_text_delegate.dart new file mode 100644 index 0000000000..03c86854a8 --- /dev/null +++ b/lib/utils/localized_camera_picker_text_delegate.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:wechat_camera_picker/wechat_camera_picker.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class LocalizedCameraPickerTextDelegate extends CameraPickerTextDelegate { + final BuildContext context; + final String language; + const LocalizedCameraPickerTextDelegate(this.context, this.language); + + @override + String get languageCode => language; + + @override + String get confirm => L10n.of(context)!.confirm; + + @override + String get shootingTips => L10n.of(context)!.shootingTips; + + @override + String get shootingWithRecordingTips => + L10n.of(context)!.shootingWithRecordingTips; + + @override + String get shootingOnlyRecordingTips => + L10n.of(context)!.shootingOnlyRecordingTips; + + @override + String get shootingTapRecordingTips => + L10n.of(context)!.shootingTapRecordingTips; + + @override + String get loadFailed => L10n.of(context)!.loadFailed; + + @override + String get loading => L10n.of(context)!.loading; + + @override + String get saving => L10n.of(context)!.saving; + + @override + String get sActionManuallyFocusHint => + L10n.of(context)!.sActionManuallyFocusHint; + + @override + String get sActionPreviewHint => L10n.of(context)!.sActionPreviewHint; + + @override + String get sActionRecordHint => L10n.of(context)!.sActionRecordHint; + + @override + String get sActionShootHint => L10n.of(context)!.sActionShootHint; + + @override + String get sActionShootingButtonTooltip => + L10n.of(context)!.sActionShootingButtonTooltip; + + @override + String get sActionStopRecordingHint => + L10n.of(context)!.sActionStopRecordingHint; + + @override + String sCameraLensDirectionLabel(CameraLensDirection value) => + L10n.of(context)!.sCameraLensDirectionLabel(value.name); + + @override + String? sCameraPreviewLabel(CameraLensDirection? value) { + if (value == null) { + return null; + } + return L10n.of(context)!.sCameraPreviewLabel(value.name); + } + + @override + String sFlashModeLabel(FlashMode mode) => + L10n.of(context)!.sFlashModeLabel(mode.name); + + @override + String sSwitchCameraLensDirectionLabel(CameraLensDirection value) => + L10n.of(context)!.sSwitchCameraLensDirectionLabel(value.name); +} diff --git a/lib/utils/manager/storage_directory_manager.dart b/lib/utils/manager/storage_directory_manager.dart index a45902f8f5..d6d30fe836 100644 --- a/lib/utils/manager/storage_directory_manager.dart +++ b/lib/utils/manager/storage_directory_manager.dart @@ -76,4 +76,15 @@ class StorageDirectoryManager { await StorageDirectoryManager.instance.getFileStoreDirectory(); return '$fileStoreDirectory/$eventId/decrypted-$fileName'; } + + String convertFileExtension(String filename, String newExtension) { + final lastDotIndex = filename.lastIndexOf('.'); + + if (lastDotIndex == -1) { + return '$filename.$newExtension'; + } else { + final nameWithoutExtension = filename.substring(0, lastDotIndex); + return '$nameWithoutExtension.$newExtension'; + } + } } diff --git a/lib/utils/manager/upload_manager/models/upload_file_info.dart b/lib/utils/manager/upload_manager/models/upload_file_info.dart index 7ca79f16a2..f8049a15a3 100644 --- a/lib/utils/manager/upload_manager/models/upload_file_info.dart +++ b/lib/utils/manager/upload_manager/models/upload_file_info.dart @@ -3,6 +3,7 @@ import 'package:dartz/dartz.dart'; import 'package:dio/dio.dart'; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; +import 'package:fluffychat/utils/manager/upload_manager/models/upload_caption_info.dart'; import 'package:fluffychat/utils/manager/upload_manager/models/upload_info.dart'; class UploadFileInfo extends UploadInfo { @@ -10,6 +11,7 @@ class UploadFileInfo extends UploadInfo { final Stream> uploadStream; final CancelToken cancelToken; final DateTime createdAt; + final UploadCaptionInfo? captionInfo; UploadFileInfo({ required super.txid, @@ -17,6 +19,7 @@ class UploadFileInfo extends UploadInfo { required this.uploadStateStreamController, required this.uploadStream, required this.cancelToken, + this.captionInfo, }); @override @@ -26,5 +29,6 @@ class UploadFileInfo extends UploadInfo { uploadStream, cancelToken, createdAt, + captionInfo, ]; } diff --git a/lib/utils/manager/upload_manager/upload_manager.dart b/lib/utils/manager/upload_manager/upload_manager.dart index 6e17bf2eab..2bab843576 100644 --- a/lib/utils/manager/upload_manager/upload_manager.dart +++ b/lib/utils/manager/upload_manager/upload_manager.dart @@ -34,11 +34,33 @@ class UploadManager { Future cancelUpload(Event event) async { final cancelToken = _eventIdMapUploadFileInfo[event.eventId]?.cancelToken; + final captionInfo = _eventIdMapUploadFileInfo[event.eventId]?.captionInfo; if (cancelToken != null) { Logs().d('Remove eventid: ${event.eventId}'); - cancelToken.cancel(); _clearFileTask(event.eventId); event.remove(); + cancelToken.cancel(); + if (captionInfo != null) { + _handleCancelCaptionEvent( + txid: captionInfo.txid, + room: event.room, + ); + } + } + } + + Future _handleCancelCaptionEvent({ + required String txid, + required Room room, + }) async { + try { + _clearCaptionTask(txid); + final captionEvent = await room.getEventById(txid); + captionEvent?.remove(); + } catch (e) { + Logs().e( + 'UploadManager::_handleCancelCaptionEvent(): $e', + ); } } @@ -50,7 +72,7 @@ class UploadManager { .close(); } catch (e) { Logs().e( - 'UploadManager::_clear(): $e', + 'UploadManager::_clear(): Error $e', ); } finally { _eventIdMapUploadFileInfo.remove(eventId); @@ -80,6 +102,8 @@ class UploadManager { void _initUploadFileInfo({ required String txid, + required Room room, + String? captionInfo, }) { final uploadController = StreamController>(); @@ -89,6 +113,12 @@ class UploadManager { uploadStream: uploadController.stream.asBroadcastStream(), cancelToken: CancelToken(), createdAt: DateTime.now(), + captionInfo: captionInfo != null && captionInfo.isNotEmpty + ? UploadCaptionInfo( + txid: room.client.generateUniqueTransactionId(), + caption: captionInfo, + ) + : null, ); } @@ -105,35 +135,40 @@ class UploadManager { entities: entities, ); - for (final txid in txids.keys) { - final fakeSendingFileInfo = txids[txid]; + for (final txid in txids.entries) { + final txidKey = txid.key; + final fakeSendingFileInfo = txids[txidKey]; if (fakeSendingFileInfo == null) { continue; } - Logs().d('UploadManager::uploadMediaMobile(): txid: $txid'); + Logs().d('UploadManager::uploadMediaMobile(): txid: $txidKey'); - _initUploadFileInfo(txid: txid); + _initUploadFileInfo( + txid: txidKey, + room: room, + captionInfo: txidKey == txids.keys.last ? caption : null, + ); - final sentDate = _eventIdMapUploadFileInfo[txid]?.createdAt; + final sentDate = _eventIdMapUploadFileInfo[txidKey]?.createdAt; final fakeImageEvent = await room.sendFakeImagePickerFileEvent( fakeSendingFileInfo.fileInfo, - txid: txid, + txid: txidKey, messageType: fakeSendingFileInfo.messageType, sentDate: sentDate, ); final streamController = - _eventIdMapUploadFileInfo[txid]?.uploadStateStreamController; + _eventIdMapUploadFileInfo[txidKey]?.uploadStateStreamController; - final cancelToken = _eventIdMapUploadFileInfo[txid]?.cancelToken; + final cancelToken = _eventIdMapUploadFileInfo[txidKey]?.cancelToken; if (streamController == null || cancelToken == null) { Logs().e( 'DownloadManager::download(): streamController or cancelToken is null', ); - _eventIdMapUploadFileInfo[txid]?.uploadStateStreamController.add( + _eventIdMapUploadFileInfo[txidKey]?.uploadStateStreamController.add( Left( UploadFileFailedState( exception: Exception( @@ -152,7 +187,7 @@ class UploadManager { ); _addFileTaskToWorkerQueueMobile( - txid: txid, + txid: txidKey, fakeImageEvent: fakeImageEvent, room: room, fileInfo: fakeSendingFileInfo.fileInfo, @@ -161,12 +196,16 @@ class UploadManager { sentDate: sentDate, shrinkImageMaxDimension: _shrinkImageMaxDimension, ); - } - if (caption != null && caption.isNotEmpty) { - _addCaptionTaskToWorkerQueue( - room: room, - caption: caption, - ); + + if (_eventIdMapUploadFileInfo[txidKey]?.captionInfo != null) { + _addCaptionTaskToWorkerQueue( + room: room, + messageTxid: + _eventIdMapUploadFileInfo[txidKey]?.captionInfo?.txid ?? '', + caption: + _eventIdMapUploadFileInfo[txidKey]?.captionInfo?.caption ?? '', + ); + } } } @@ -176,14 +215,20 @@ class UploadManager { Map? thumbnails, String? caption, }) async { - for (final MatrixFile matrixFile in files) { + for (final matrixFile in files.asMap().entries) { final txid = room.client.generateUniqueTransactionId(); + final fileIndex = matrixFile.key; + final fileInfo = matrixFile.value; - _initUploadFileInfo(txid: txid); + _initUploadFileInfo( + txid: txid, + room: room, + captionInfo: fileIndex == files.length - 1 ? caption : null, + ); - room.sendingFilePlaceholders[txid] = matrixFile; + room.sendingFilePlaceholders[txid] = fileInfo; final fakeFileEvent = await room.sendFakeFileEvent( - matrixFile, + fileInfo, txid: txid, ); @@ -220,19 +265,19 @@ class UploadManager { txid: txid, fakeImageEvent: fakeFileEvent, room: room, - matrixFile: matrixFile, + matrixFile: fileInfo, streamController: streamController, cancelToken: cancelToken, - thumbnail: thumbnails?[matrixFile], + thumbnail: thumbnails?[fileInfo], sentDate: sentDate, ); - } - - if (caption != null && caption.isNotEmpty) { - _addCaptionTaskToWorkerQueue( - room: room, - caption: caption, - ); + if (_eventIdMapUploadFileInfo[txid]?.captionInfo != null) { + _addCaptionTaskToWorkerQueue( + room: room, + messageTxid: _eventIdMapUploadFileInfo[txid]?.captionInfo?.txid ?? '', + caption: _eventIdMapUploadFileInfo[txid]?.captionInfo?.caption ?? '', + ); + } } } @@ -241,21 +286,28 @@ class UploadManager { required List fileInfos, String? caption, }) async { - for (final fileInfo in fileInfos) { + for (final fileInfo in fileInfos.asMap().entries) { + final fileIndex = fileInfo.key; + final fileValue = fileInfo.value; + final txid = room.storePlaceholderFileInMem( - fileInfo: fileInfo, + fileInfo: fileValue, ); Logs().d('UploadManager::uploadFileMobile(): txid: $txid'); - _initUploadFileInfo(txid: txid); + _initUploadFileInfo( + txid: txid, + room: room, + captionInfo: fileIndex == fileInfos.length - 1 ? caption : null, + ); final sentDate = _eventIdMapUploadFileInfo[txid]?.createdAt; final fakeEvent = await room.sendFakeImagePickerFileEvent( - fileInfo, + fileValue, txid: txid, - messageType: fileInfo.msgType, + messageType: fileValue.msgType, sentDate: sentDate, ); @@ -290,26 +342,30 @@ class UploadManager { txid: txid, fakeImageEvent: fakeEvent, room: room, - fileInfo: fileInfo, + fileInfo: fileValue, streamController: streamController, cancelToken: cancelToken, sentDate: sentDate, ); - } - if (caption != null && caption.isNotEmpty) { - _addCaptionTaskToWorkerQueue( - room: room, - caption: caption, - ); + if (_eventIdMapUploadFileInfo[txid]?.captionInfo != null) { + _addCaptionTaskToWorkerQueue( + room: room, + messageTxid: _eventIdMapUploadFileInfo[txid]?.captionInfo?.txid ?? '', + caption: _eventIdMapUploadFileInfo[txid]?.captionInfo?.caption ?? '', + ); + } } } Future _addCaptionTaskToWorkerQueue({ required Room room, - required String caption, + String? messageTxid, + String? caption, }) async { - final messageTxid = room.client.generateUniqueTransactionId(); - + if ((messageTxid == null && messageTxid!.isEmpty) || + (caption == null && caption!.isEmpty)) { + return; + } final messageContent = room.getEventContentFromMsgText(message: caption); _initUploadCaptionInfo( diff --git a/lib/utils/matrix_sdk_extensions/download_file_extension.dart b/lib/utils/matrix_sdk_extensions/download_file_extension.dart index 1b8483cd54..daf0f62690 100644 --- a/lib/utils/matrix_sdk_extensions/download_file_extension.dart +++ b/lib/utils/matrix_sdk_extensions/download_file_extension.dart @@ -276,6 +276,10 @@ extension DownloadFileExtension on Event { throw "getFileInfo: This event hasn't any attachment or thumbnail."; } + if (getThumbnail && thumbnailMimetype.startsWith('image') != true) { + throw ('getFileInfo: This event has a thumbnail but it is not an image.'); + } + final isFileEncrypted = getThumbnail ? isThumbnailEncrypted : isAttachmentEncrypted; if (isEncryptionDisabled(isFileEncrypted)) { diff --git a/lib/utils/permission_dialog.dart b/lib/utils/permission_dialog.dart index 59aa1ed099..0afc55a7f7 100644 --- a/lib/utils/permission_dialog.dart +++ b/lib/utils/permission_dialog.dart @@ -3,6 +3,7 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; typedef OnAcceptButton = void Function()?; +typedef OnRefuseTap = void Function()?; class PermissionDialog extends StatefulWidget { final Permission permission; @@ -13,12 +14,15 @@ class PermissionDialog extends StatefulWidget { final OnAcceptButton onAcceptButton; + final OnRefuseTap onRefuseTap; + const PermissionDialog({ super.key, required this.permission, required this.explainTextRequestPermission, this.icon, this.onAcceptButton, + this.onRefuseTap, }); @override @@ -49,31 +53,31 @@ class _PermissionDialogState extends State borderRadius: BorderRadius.circular(28.0), color: Theme.of(context).colorScheme.surface, ), - width: 312, - height: 280, + width: MediaQuery.sizeOf(context).width * 0.85, padding: const EdgeInsets.symmetric(horizontal: 24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (widget.icon != null) ...[ + child: IntrinsicHeight( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (widget.icon != null) ...[ + const SizedBox( + height: 24.0, + ), + widget.icon!, + ], const SizedBox( - height: 24.0, + height: 16.0, ), - widget.icon!, - ], - const SizedBox( - height: 16.0, - ), - widget.explainTextRequestPermission, - const SizedBox(height: 24.0), - Expanded( - child: Row( + widget.explainTextRequestPermission, + const SizedBox(height: 24.0), + Row( mainAxisAlignment: MainAxisAlignment.end, children: [ _PermissionTextButton( context: context, text: L10n.of(context)!.deny, onPressed: () { + widget.onRefuseTap?.call(); Navigator.of(context).pop(); }, ), @@ -92,8 +96,9 @@ class _PermissionDialogState extends State ), ], ), - ), - ], + const SizedBox(height: 24.0), + ], + ), ), ), ), diff --git a/lib/utils/permission_service.dart b/lib/utils/permission_service.dart index 795dd2c6d7..6a44251159 100644 --- a/lib/utils/permission_service.dart +++ b/lib/utils/permission_service.dart @@ -1,5 +1,8 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:fluffychat/utils/permission_dialog.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; class PermissionHandlerService { @@ -14,14 +17,16 @@ class PermissionHandlerService { PermissionHandlerService._internal(); - Future? requestPermissionForMediaActions() async { + Future? requestPermissionForMediaActions( + BuildContext context, + ) async { if (Platform.isIOS) { - return _handlePhotosPermissionIOSAction(); + return _handlePhotosPermissionIOSAction(context); } else if (Platform.isAndroid) { if (await _getCurrentAndroidVersion() >= 33) { - return _handleMediaPickerPermissionAndroidHigher33Action(); + return _handleMediaPickerPermissionAndroidHigher33Action(context); } - return _handleMediaPermissionAndroidAction(); + return _handleMediaPermissionAndroidAction(context); } else { return null; } @@ -45,7 +50,7 @@ class PermissionHandlerService { } } - Future requestPermissionForMircoActions() async { + Future requestPermissionForMicroActions() async { final currentStatus = await Permission.microphone.status; if (currentStatus == PermissionStatus.denied || currentStatus == PermissionStatus.permanentlyDenied) { @@ -55,28 +60,83 @@ class PermissionHandlerService { } } - Future _handlePhotosPermissionIOSAction() async { + Future _handlePhotosPermissionIOSAction( + BuildContext context, + ) async { final currentStatus = await Permission.photos.status; - return _handlePhotoPermission(currentStatus); + return _handlePhotoPermission( + currentStatus: currentStatus, + context: context, + ); } - Future _handleMediaPermissionAndroidAction() async { + Future _handleMediaPermissionAndroidAction( + BuildContext context, + ) async { final currentStatus = await Permission.storage.status; - return _handlePhotoPermission(currentStatus); + return _handlePhotoPermission( + currentStatus: currentStatus, + context: context, + ); } - Future - _handleMediaPickerPermissionAndroidHigher33Action() async { - PermissionStatus? photoPermission = await Permission.photos.status; - if (photoPermission == PermissionStatus.denied) { - photoPermission = await Permission.photos.request(); + Future _handleMediaPickerPermissionAndroidHigher33Action( + BuildContext context, + ) async { + if (await Permission.photos.status == PermissionStatus.denied) { + final result = await showDialog( + useRootNavigator: false, + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return PermissionDialog( + icon: const Icon(Icons.photo), + permission: Permission.photos, + explainTextRequestPermission: Text( + L10n.of(context)!.explainPermissionToAccessPhotos, + style: Theme.of(context).textTheme.bodyMedium, + ), + onAcceptButton: () async { + Navigator.of(dialogContext).pop(true); + }, + ); + }, + ); + + if (result != null && result) { + final newStatus = await Permission.photos.request(); + return newStatus; + } } - PermissionStatus? videosPermission = await Permission.videos.status; - if (videosPermission == PermissionStatus.denied) { - videosPermission = await Permission.videos.request(); + if (await Permission.videos.status == PermissionStatus.denied) { + final result = await showDialog( + useRootNavigator: false, + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return PermissionDialog( + icon: const Icon(Icons.video_camera_back_outlined), + permission: Permission.videos, + explainTextRequestPermission: Text( + L10n.of(context)!.explainPermissionToAccessVideos, + style: Theme.of(context).textTheme.bodyMedium, + ), + onAcceptButton: () async { + Navigator.of(dialogContext).pop(true); + }, + ); + }, + ); + if (result != null && result) { + final newStatus = await Permission.videos.request(); + return newStatus; + } } + final photoPermission = await Permission.photos.status; + final videosPermission = await Permission.videos.status; + if (photoPermission == PermissionStatus.granted || videosPermission == PermissionStatus.granted) { return PermissionStatus.granted; @@ -85,16 +145,41 @@ class PermissionHandlerService { return PermissionStatus.denied; } - Future _handlePhotoPermission( - PermissionStatus currentStatus, - ) async { + Future _handlePhotoPermission({ + required PermissionStatus currentStatus, + required BuildContext context, + }) async { switch (currentStatus) { case PermissionStatus.permanentlyDenied: case PermissionStatus.denied: - final newStatus = Platform.isIOS - ? await Permission.photos.request() - : await Permission.storage.request(); - return newStatus.isGranted ? PermissionStatus.granted : newStatus; + final result = await showDialog( + useRootNavigator: false, + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return PermissionDialog( + icon: const Icon(Icons.photo), + permission: + Platform.isIOS ? Permission.photos : Permission.storage, + explainTextRequestPermission: Text( + L10n.of(context)!.explainPermissionToAccessMedias, + style: Theme.of(context).textTheme.bodyMedium, + ), + onAcceptButton: () async { + Navigator.of(dialogContext).pop(true); + }, + ); + }, + ); + if (result != null && result) { + final newStatus = Platform.isIOS + ? await Permission.photos.request() + : await Permission.storage.request(); + + return newStatus.isGranted ? PermissionStatus.granted : newStatus; + } else { + return currentStatus; + } case PermissionStatus.granted: case PermissionStatus.limited: @@ -118,10 +203,12 @@ class PermissionHandlerService { Future requestContactsPermissionActions() async { final currentStatus = await contactsPermissionStatus; - if (currentStatus == PermissionStatus.denied || - currentStatus == PermissionStatus.permanentlyDenied) { + if (currentStatus == PermissionStatus.denied) { final newStatus = await Permission.contacts.request(); return newStatus.isGranted ? PermissionStatus.granted : newStatus; + } else if (currentStatus == PermissionStatus.permanentlyDenied) { + goToSettingsForPermissionActions(); + return await contactsPermissionStatus; } else { return currentStatus; } diff --git a/lib/utils/responsive/responsive_utils.dart b/lib/utils/responsive/responsive_utils.dart index 74f63f2e4b..04f1e89a11 100644 --- a/lib/utils/responsive/responsive_utils.dart +++ b/lib/utils/responsive/responsive_utils.dart @@ -18,7 +18,7 @@ class ResponsiveUtils { static const double defaultSizeBodyLayoutDesktop = 280; static const double heightBottomNavigation = 72; - static const double heightBottomNavigationBar = 56; + static const double heightBottomNavigationBar = 48; static const double bodyWithRightColumnRatio = 0.64; static const double groupDetailsMinWidth = 370; diff --git a/lib/utils/string_extension.dart b/lib/utils/string_extension.dart index c688a6d20e..02d326a305 100644 --- a/lib/utils/string_extension.dart +++ b/lib/utils/string_extension.dart @@ -251,10 +251,13 @@ extension StringCasingExtension on String { ]; } + // Escape special characters in the highlightText + final escapedHighlightText = RegExp.escape(highlightText); + // Split the text into parts by the search word and create a TextSpan for // each part. The search word is not case sensitive. final List spans = splitMapJoinToList( - RegExp(highlightText, caseSensitive: false), + RegExp(escapedHighlightText, caseSensitive: false), onMatch: (Match match) { return TextSpan( text: match.group(0), diff --git a/lib/utils/voip/callkeep_manager.dart b/lib/utils/voip/callkeep_manager.dart index 32a9c1ac00..479769c364 100644 --- a/lib/utils/voip/callkeep_manager.dart +++ b/lib/utils/voip/callkeep_manager.dart @@ -111,8 +111,7 @@ class CallKeepManager { Future showCallkitIncoming(CallSession call) async { if (!setupDone) { await _callKeep.setup( - null, - { + options: { 'ios': { 'appName': appName, }, @@ -157,8 +156,8 @@ class CallKeepManager { } void didDisplayIncomingCall(CallKeepDidDisplayIncomingCall event) { - final callUUID = event.callUUID; - final number = event.handle; + final callUUID = event.callData.callUUID; + final number = event.callData.handle; Logs().v('[displayIncomingCall] $callUUID number: $number'); // addCall(callUUID, CallKeeper(this null)); } @@ -168,20 +167,18 @@ class CallKeepManager { } Future initialize() async { - _callKeep.on(CallKeepPerformAnswerCallAction(), answerCall); - _callKeep.on(CallKeepDidPerformDTMFAction(), didPerformDTMFAction); - _callKeep.on( - CallKeepDidReceiveStartCallAction(), + _callKeep.on(answerCall); + _callKeep.on(didPerformDTMFAction); + _callKeep.on( didReceiveStartCallAction, ); - _callKeep.on(CallKeepDidToggleHoldAction(), didToggleHoldCallAction); - _callKeep.on( - CallKeepDidPerformSetMutedCallAction(), + _callKeep.on(didToggleHoldCallAction); + _callKeep.on( didPerformSetMutedCallAction, ); - _callKeep.on(CallKeepPerformEndCallAction(), endCall); - _callKeep.on(CallKeepPushKitToken(), onPushKitToken); - _callKeep.on(CallKeepDidDisplayIncomingCall(), didDisplayIncomingCall); + _callKeep.on(endCall); + _callKeep.on(onPushKitToken); + _callKeep.on(didDisplayIncomingCall); Logs().i('[VOIP] Initialized'); } @@ -203,12 +200,12 @@ class CallKeepManager { } Future setOnHold(String callUUID, bool held) async { - await _callKeep.setOnHold(callUUID, held); + await _callKeep.setOnHold(uuid: callUUID, shouldHold: held); setCallHeld(callUUID, held); } Future setMutedCall(String callUUID, bool muted) async { - await _callKeep.setMutedCall(callUUID, muted); + await _callKeep.setMutedCall(uuid: callUUID, shouldMute: muted); setCallMuted(callUUID, muted); } @@ -216,14 +213,14 @@ class CallKeepManager { // Workaround because Android doesn't display well displayName, se we have to switch ... if (isIOS) { await _callKeep.updateDisplay( - callUUID, - displayName: 'New Name', + uuid: callUUID, + callerName: 'New Name', handle: callUUID, ); } else { await _callKeep.updateDisplay( - callUUID, - displayName: callUUID, + uuid: callUUID, + callerName: callUUID, handle: 'New Name', ); } @@ -233,10 +230,9 @@ class CallKeepManager { final callKeeper = CallKeeper(this, call); addCall(call.callId, callKeeper); await _callKeep.displayIncomingCall( - call.callId, - '${call.room.getLocalizedDisplayname()} (Twake Chat)', - localizedCallerName: - '${call.room.getLocalizedDisplayname()} (Twake Chat)', + uuid: call.callId, + handle: '${call.room.getLocalizedDisplayname()} (Twake Chat)', + callerName: '${call.room.getLocalizedDisplayname()} (Twake Chat)', handleType: 'number', hasVideo: call.type == CallType.kVideo, ); @@ -281,16 +277,18 @@ class CallKeepManager { } void openCallingAccountsPage(BuildContext context) async { - await _callKeep.setup(context, { - 'ios': { - 'appName': appName, + await _callKeep.setup( + options: { + 'ios': { + 'appName': appName, + }, + 'android': alertOptions, }, - 'android': alertOptions, - }); + ); final hasPhoneAccount = await _callKeep.hasPhoneAccount(); Logs().e(hasPhoneAccount.toString()); if (!hasPhoneAccount) { - await _callKeep.hasDefaultPhoneAccount(context, alertOptions); + await _callKeep.hasDefaultPhoneAccount(alertOptions); } else { await _callKeep.openPhoneAccounts(); } @@ -298,8 +296,8 @@ class CallKeepManager { /// CallActions. Future answerCall(CallKeepPerformAnswerCallAction event) async { - final callUUID = event.callUUID; - final keeper = calls[event.callUUID]!; + final callUUID = event.callData.callUUID; + final keeper = calls[event.callData.callUUID]!; if (!keeper.connected) { Logs().e('answered'); // Answer Call @@ -325,17 +323,21 @@ class CallKeepManager { Future didReceiveStartCallAction( CallKeepDidReceiveStartCallAction event, ) async { - if (event.handle == null) { + if (event.callData.handle == null) { // @TODO: sometime we receive `didReceiveStartCallAction` with handle` undefined` return; } - final callUUID = event.callUUID!; - if (event.callUUID == null) { - final call = - await _voipPlugin!.voip.inviteToCall(event.handle!, CallType.kVideo); + final callUUID = event.callData.callUUID!; + if (event.callData.callUUID == null) { + final call = await _voipPlugin!.voip + .inviteToCall(event.callData.handle!, CallType.kVideo); addCall(callUUID, CallKeeper(this, call)); } - await _callKeep.startCall(callUUID, event.handle!, event.handle!); + await _callKeep.startCall( + uuid: callUUID, + handle: event.callData.handle!, + callerName: event.callData.handle!, + ); Timer(const Duration(seconds: 1), () { _callKeep.setCurrentCallActive(callUUID); }); diff --git a/lib/utils/voip/user_media_manager.dart b/lib/utils/voip/user_media_manager.dart index 874da93ee1..5d7cd39401 100644 --- a/lib/utils/voip/user_media_manager.dart +++ b/lib/utils/voip/user_media_manager.dart @@ -16,9 +16,11 @@ class UserMediaManager { AudioPlayer? _assetsAudioPlayer; + static final _flutterRingtonePlayer = FlutterRingtonePlayer(); + Future startRingingTone() async { if (PlatformInfos.isMobile) { - await FlutterRingtonePlayer.playRingtone(volume: 80); + await _flutterRingtonePlayer.playRingtone(volume: 80); } else if ((kIsWeb || PlatformInfos.isMacOS) && _assetsAudioPlayer != null) { const path = 'assets/sounds/phone.ogg'; @@ -31,7 +33,7 @@ class UserMediaManager { Future stopRingingTone() async { if (PlatformInfos.isMobile) { - await FlutterRingtonePlayer.stop(); + await _flutterRingtonePlayer.stop(); } await _assetsAudioPlayer?.stop(); _assetsAudioPlayer = null; diff --git a/lib/widgets/app_bars/searchable_app_bar.dart b/lib/widgets/app_bars/searchable_app_bar.dart index 865bd56469..e4674a87d9 100644 --- a/lib/widgets/app_bars/searchable_app_bar.dart +++ b/lib/widgets/app_bars/searchable_app_bar.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/config/first_column_inner_routes.dart'; import 'package:fluffychat/pages/dialer/pip/dismiss_keyboard.dart'; +import 'package:fluffychat/widgets/context_menu_builder_ios_paste_without_permission.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:fluffychat/widgets/app_bars/searchable_app_bar_style.dart'; import 'package:flutter/material.dart'; @@ -185,6 +186,7 @@ class SearchableAppBar extends StatelessWidget { focusNode: focusNode, autofocus: true, maxLines: SearchableAppBarStyle.textFieldMaxLines, + contextMenuBuilder: mobileTwakeContextMenuBuilder, buildCounter: ( BuildContext context, { required int currentLength, diff --git a/lib/widgets/avatar/bottom_navigation_avatar.dart b/lib/widgets/avatar/bottom_navigation_avatar.dart new file mode 100644 index 0000000000..6d45ea4f71 --- /dev/null +++ b/lib/widgets/avatar/bottom_navigation_avatar.dart @@ -0,0 +1,43 @@ +import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/widgets/avatar/bottom_navigation_avatar_style.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; +import 'package:matrix/matrix.dart'; + +class BottomNavigationAvatar extends StatelessWidget { + final bool isSelected; + final ValueNotifier profile; + + const BottomNavigationAvatar({ + super.key, + required this.isSelected, + required this.profile, + }); + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: profile, + builder: (context, profile, child) { + return Container( + decoration: isSelected + ? ShapeDecoration( + shape: CircleBorder( + side: BorderSide( + width: + BottomNavigationAvatarStyle.selectedavatarBorderWidth, + color: LinagoraSysColors.material().primary, + ), + ), + ) + : null, + child: Avatar( + name: profile?.displayName, + mxContent: profile?.avatarUrl, + size: BottomNavigationAvatarStyle.avatarSize, + fontSize: BottomNavigationAvatarStyle.avatarFontSize, + ), + ); + }, + ); + } +} diff --git a/lib/widgets/avatar/bottom_navigation_avatar_style.dart b/lib/widgets/avatar/bottom_navigation_avatar_style.dart new file mode 100644 index 0000000000..74cc3ed998 --- /dev/null +++ b/lib/widgets/avatar/bottom_navigation_avatar_style.dart @@ -0,0 +1,5 @@ +class BottomNavigationAvatarStyle { + static const double avatarSize = 24.0; + static const double avatarFontSize = 10.0; + static const double selectedavatarBorderWidth = 2.0; +} diff --git a/lib/widgets/contacts_warning_banner/contacts_warning_banner_view.dart b/lib/widgets/contacts_warning_banner/contacts_warning_banner_view.dart index 51cb19d477..85f01850ee 100644 --- a/lib/widgets/contacts_warning_banner/contacts_warning_banner_view.dart +++ b/lib/widgets/contacts_warning_banner/contacts_warning_banner_view.dart @@ -42,7 +42,7 @@ class ContactsWarningBannerView extends StatelessWidget { Padding( padding: ContactsWarningBannerStyle.paddingForContentBanner, child: Text( - L10n.of(context)!.contactsWarningBannerTitle, + L10n.of(context)!.explainPermissionToAccessContacts, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface, ), diff --git a/lib/widgets/context_menu_builder_ios_paste_without_permission.dart b/lib/widgets/context_menu_builder_ios_paste_without_permission.dart new file mode 100644 index 0000000000..d0cdf4e840 --- /dev/null +++ b/lib/widgets/context_menu_builder_ios_paste_without_permission.dart @@ -0,0 +1,15 @@ +import 'package:flutter/material.dart'; + +Widget mobileTwakeContextMenuBuilder( + BuildContext context, + EditableTextState editableTextState, +) { + if (SystemContextMenu.isSupported(context)) { + return SystemContextMenu.editableText( + editableTextState: editableTextState, + ); + } + return AdaptiveTextSelectionToolbar.editableText( + editableTextState: editableTextState, + ); +} diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_style.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_style.dart index 5fcde1a526..29b2c06663 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_style.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_style.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; class AdaptiveScaffoldPrimaryNavigationStyle { static const EdgeInsetsDirectional primaryNavigationMargin = @@ -13,6 +14,13 @@ class AdaptiveScaffoldPrimaryNavigationStyle { ); } + static TextStyle? selectedLabelTextStyle(BuildContext context) { + return Theme.of(context).textTheme.labelMedium?.copyWith( + color: LinagoraSysColors.material().primary, + overflow: TextOverflow.ellipsis, + ); + } + static const double primaryNavigationWidth = 80; static const double avatarSize = 56; static const double dividerSize = 2; diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_view.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_view.dart index 5e210b2826..ffd73abd36 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_view.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation_view.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_pri import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; import 'package:matrix/matrix.dart'; class AdaptiveScaffoldPrimaryNavigationView extends StatelessWidget { @@ -25,7 +26,6 @@ class AdaptiveScaffoldPrimaryNavigationView extends StatelessWidget { return Material( color: Theme.of(context).colorScheme.surface, child: Container( - margin: AdaptiveScaffoldPrimaryNavigationStyle.primaryNavigationMargin, width: AdaptiveScaffoldPrimaryNavigationStyle.primaryNavigationWidth, decoration: BoxDecoration( color: Theme.of(context).colorScheme.surface, @@ -40,14 +40,15 @@ class AdaptiveScaffoldPrimaryNavigationView extends StatelessWidget { onDestinationSelected: onDestinationSelected, labelType: NavigationRailLabelType.all, backgroundColor: Theme.of(context).colorScheme.surface, - selectedLabelTextStyle: - AdaptiveScaffoldPrimaryNavigationStyle.labelTextStyle( + selectedLabelTextStyle: AdaptiveScaffoldPrimaryNavigationStyle + .selectedLabelTextStyle( context, ), unselectedLabelTextStyle: AdaptiveScaffoldPrimaryNavigationStyle.labelTextStyle( context, ), + indicatorColor: LinagoraSysColors.material().secondaryContainer, ), ), Column( diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart index 2c06f52fb2..b8b037bee9 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:fluffychat/config/first_column_inner_routes.dart'; +import 'package:fluffychat/event/twake_inapp_event_types.dart'; import 'package:fluffychat/presentation/enum/settings/settings_action_enum.dart'; import 'package:fluffychat/presentation/mixins/connect_page_mixin.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; @@ -6,7 +9,9 @@ import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart'; import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; +import 'package:fluffychat/widgets/layouts/agruments/logged_in_other_account_body_args.dart'; import 'package:fluffychat/widgets/layouts/agruments/logout_body_args.dart'; +import 'package:fluffychat/widgets/layouts/agruments/receive_content_args.dart'; import 'package:fluffychat/widgets/layouts/agruments/switch_active_account_body_args.dart'; import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -42,6 +47,8 @@ class AppAdaptiveScaffoldBodyController extends State ValueNotifier(AdaptiveDestinationEnum.rooms); final activeRoomIdNotifier = ValueNotifier(null); + final currentProfileNotifier = ValueNotifier(Profile(userId: '')); + StreamSubscription? onAccountDataSubscription; final PageController pageController = PageController(initialPage: 1, keepPage: true); @@ -117,20 +124,56 @@ class AppAdaptiveScaffoldBodyController extends State void _handleLogout(AppAdaptiveScaffoldBody oldWidget) { activeNavigationBarNotifier.value = AdaptiveDestinationEnum.rooms; pageController.jumpToPage(AdaptiveDestinationEnum.rooms.index); + getCurrentProfile(); + onAccountDataSubscription?.cancel(); + _handleProfileDataChange(); } void _handleSwitchAccount(AppAdaptiveScaffoldBody oldWidget) { activeNavigationBarNotifier.value = AdaptiveDestinationEnum.rooms; pageController.jumpToPage(AdaptiveDestinationEnum.rooms.index); + getCurrentProfile(); + onAccountDataSubscription?.cancel(); + _handleProfileDataChange(); + } + + void getCurrentProfile() async { + final profile = + await matrix.client.fetchOwnProfile(getFromRooms: false, cache: false); + currentProfileNotifier.value = profile; + } + + void _handleProfileDataChange() { + onAccountDataSubscription = + matrix.client.onAccountData.stream.listen((event) { + if (event.type == TwakeInappEventTypes.uploadAvatarEvent) { + getCurrentProfile(); + } + }); + } + + void _handleReceiveContent(ReceiveContentArgs args) { + if (args.activeDestination == null) return; + if (args.activeDestination != AdaptiveDestinationEnum.rooms) { + activeNavigationBarNotifier.value = AdaptiveDestinationEnum.rooms; + pageController.jumpToPage(AdaptiveDestinationEnum.rooms.index); + } } MatrixState get matrix => Matrix.of(context); @override void initState() { + super.initState(); activeRoomIdNotifier.value = widget.activeRoomId; resetLocationPathWithLoginToken(); - super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (mounted) { + await matrix.retrievePersistedActiveClient(); + getCurrentProfile(); + _handleProfileDataChange(); + } + }); } @override @@ -146,10 +189,20 @@ class AppAdaptiveScaffoldBodyController extends State _handleLogout(oldWidget); } + if (oldWidget.args != widget.args && + widget.args is LoggedInOtherAccountBodyArgs) { + getCurrentProfile(); + _handleProfileDataChange(); + } + if (oldWidget.args != widget.args && widget.args is SwitchActiveAccountBodyArgs) { _handleSwitchAccount(oldWidget); } + + if (widget.args is ReceiveContentArgs) { + _handleReceiveContent(widget.args as ReceiveContentArgs); + } super.didUpdateWidget(oldWidget); } @@ -158,6 +211,8 @@ class AppAdaptiveScaffoldBodyController extends State activeRoomIdNotifier.dispose(); activeNavigationBarNotifier.dispose(); pageController.dispose(); + currentProfileNotifier.dispose(); + onAccountDataSubscription?.cancel(); super.dispose(); } @@ -172,5 +227,6 @@ class AppAdaptiveScaffoldBodyController extends State onPopInvoked: _onPopInvoked, onOpenSettings: _onOpenSettingsPage, adaptiveScaffoldBodyArgs: widget.args, + currentProfile: currentProfileNotifier, ); } diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart index 772c5d3b5c..2d5de0824d 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view.dart @@ -14,6 +14,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart' hide WidgetBuilder; +import 'package:matrix/matrix.dart'; class AppAdaptiveScaffoldBodyView extends StatelessWidget { final List destinations; @@ -24,7 +25,7 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { final OnPopInvoked onPopInvoked; final VoidCallback onOpenSettings; final AbsAppAdaptiveScaffoldBodyArgs? adaptiveScaffoldBodyArgs; - + final ValueNotifier currentProfile; final ValueNotifier activeRoomIdNotifier; static const ValueKey scaffoldWithNestedNavigationKey = @@ -46,6 +47,7 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { required this.onPopInvoked, required this.onOpenSettings, this.adaptiveScaffoldBodyArgs, + required this.currentProfile, }) : super(key: key ?? scaffoldWithNestedNavigationKey); @override @@ -59,6 +61,7 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { builder: (context, activeNavigationBar, __) { return PopScope( canPop: activeNavigationBar == AdaptiveDestinationEnum.rooms, + // ignore: deprecated_member_use onPopInvoked: onPopInvoked, child: Row( children: [ @@ -126,6 +129,7 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { onOpenSettings: onOpenSettings, adaptiveScaffoldBodyArgs: adaptiveScaffoldBodyArgs, + currentProfile: currentProfile, ); }, ); @@ -152,6 +156,7 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { bottomNavigationKey: bottomNavigationKey, onOpenSettings: onOpenSettings, adaptiveScaffoldBodyArgs: adaptiveScaffoldBodyArgs, + currentProfile: currentProfile, ), ), ], @@ -163,9 +168,23 @@ class AppAdaptiveScaffoldBodyView extends StatelessWidget { ); } + List getNavigationDestinationsForBottomNavigation( + BuildContext context, + ) { + return destinations.map((destination) { + return destination.getNavigationDestinationForBottomBar( + context, + currentProfile, + ); + }).toList(); + } + List getNavigationDestinations(BuildContext context) { return destinations.map((destination) { - return destination.getNavigationDestination(context); + return destination.getNavigationDestination( + context, + currentProfile, + ); }).toList(); } } @@ -180,6 +199,7 @@ class _ColumnPageView extends StatelessWidget { final ValueNotifier activeRoomIdNotifier; final VoidCallback onOpenSettings; final AbsAppAdaptiveScaffoldBodyArgs? adaptiveScaffoldBodyArgs; + final ValueNotifier currentProfile; const _ColumnPageView({ required this.activeNavigationBarNotifier, @@ -191,6 +211,7 @@ class _ColumnPageView extends StatelessWidget { required this.bottomNavigationKey, required this.onOpenSettings, required this.adaptiveScaffoldBodyArgs, + required this.currentProfile, }); @override @@ -248,20 +269,23 @@ class _ColumnPageView extends StatelessWidget { key: bottomNavigationKey, builder: (_) { return Container( + decoration: AppAdaptiveScaffoldBodyViewStyle.navBarBorder, height: ResponsiveUtils.heightBottomNavigation, - color: LinagoraSysColors.material().surface, padding: AppAdaptiveScaffoldBodyViewStyle.paddingBottomNavigation, child: ListView( - padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), children: [ - NavigationBar( - backgroundColor: LinagoraSysColors.material().surface, - elevation: AppAdaptiveScaffoldBodyViewStyle.elevation, - height: ResponsiveUtils.heightBottomNavigationBar, - selectedIndex: _getActiveBottomNavigationBarIndex(), - destinations: getNavigationDestinations(context), - onDestinationSelected: onDestinationSelected, + Theme( + data: Theme.of(context).copyWith( + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ), + child: BottomNavigationBar( + elevation: AppAdaptiveScaffoldBodyViewStyle.elevation, + currentIndex: _getActiveBottomNavigationBarIndex(), + onTap: onDestinationSelected, + items: getNavigationDestinationsForBottomBar(context), + ), ), ], ), @@ -274,7 +298,21 @@ class _ColumnPageView extends StatelessWidget { List getNavigationDestinations(BuildContext context) { return destinations.map((destination) { - return destination.getNavigationDestination(context); + return destination.getNavigationDestination( + context, + currentProfile, + ); + }).toList(); + } + + List getNavigationDestinationsForBottomBar( + BuildContext context, + ) { + return destinations.map((destination) { + return destination.getNavigationDestinationForBottomBar( + context, + currentProfile, + ); }).toList(); } diff --git a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view_style.dart b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view_style.dart index fd4f6d81db..f4fc11a025 100644 --- a/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view_style.dart +++ b/lib/widgets/layouts/adaptive_layout/app_adaptive_scaffold_body_view_style.dart @@ -1,4 +1,6 @@ import 'package:flutter/cupertino.dart'; +import 'package:linagora_design_flutter/colors/linagora_state_layer.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; class AppAdaptiveScaffoldBodyViewStyle { static const double elevation = 0.0; @@ -6,4 +8,15 @@ class AppAdaptiveScaffoldBodyViewStyle { static const EdgeInsets paddingBottomNavigation = EdgeInsets.only( top: 4, ); + + static BoxDecoration navBarBorder = BoxDecoration( + color: LinagoraSysColors.material().surface, + border: Border( + top: BorderSide( + color: LinagoraStateLayer( + LinagoraSysColors.material().surfaceTint, + ).opacityLayer3, + ), + ), + ); } diff --git a/lib/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart b/lib/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart index c35cdc65d7..62f2bd2796 100644 --- a/lib/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart +++ b/lib/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart @@ -1,7 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:matrix/matrix.dart'; -abstract class AbsAppAdaptiveScaffoldBodyArgs extends Equatable { +abstract class AbsAppAdaptiveScaffoldBodyArgs with EquatableMixin { final Client? newActiveClient; const AbsAppAdaptiveScaffoldBodyArgs({ diff --git a/lib/widgets/layouts/agruments/receive_content_args.dart b/lib/widgets/layouts/agruments/receive_content_args.dart new file mode 100644 index 0000000000..c61cc5121b --- /dev/null +++ b/lib/widgets/layouts/agruments/receive_content_args.dart @@ -0,0 +1,17 @@ +import 'package:fluffychat/widgets/layouts/agruments/app_adaptive_scaffold_body_args.dart'; +import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; + +class ReceiveContentArgs extends AbsAppAdaptiveScaffoldBodyArgs { + const ReceiveContentArgs({ + required super.newActiveClient, + this.activeDestination, + }); + + final AdaptiveDestinationEnum? activeDestination; + + @override + List get props => [ + newActiveClient, + activeDestination, + ]; +} diff --git a/lib/widgets/layouts/enum/adaptive_destinations_enum.dart b/lib/widgets/layouts/enum/adaptive_destinations_enum.dart index 7a93b27376..4cf2ee8137 100644 --- a/lib/widgets/layouts/enum/adaptive_destinations_enum.dart +++ b/lib/widgets/layouts/enum/adaptive_destinations_enum.dart @@ -1,34 +1,55 @@ import 'package:fluffychat/utils/matrix_sdk_extensions/client_stories_extension.dart'; +import 'package:fluffychat/widgets/avatar/bottom_navigation_avatar.dart'; import 'package:fluffychat/widgets/twake_components/twake_navigation_icon/twake_navigation_icon.dart'; import 'package:fluffychat/widgets/unread_rooms_badge.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:linagora_design_flutter/colors/linagora_sys_colors.dart'; +import 'package:matrix/matrix.dart'; enum AdaptiveDestinationEnum { contacts, rooms, settings; - NavigationDestination getNavigationDestination(BuildContext context) { + NavigationDestination getNavigationDestination( + BuildContext context, + ValueNotifier profile, + ) { switch (this) { case AdaptiveDestinationEnum.contacts: return NavigationDestination( - icon: const TwakeNavigationIcon( - icon: Icons.contacts_outlined, + icon: TwakeNavigationIcon( + color: LinagoraSysColors.material().onBackground, + icon: Icons.supervised_user_circle_outlined, ), label: L10n.of(context)!.contacts, + selectedIcon: const TwakeNavigationIcon( + icon: Icons.supervised_user_circle_outlined, + isSelected: true, + ), ); case AdaptiveDestinationEnum.rooms: return NavigationDestination( icon: UnreadRoomsBadge( + color: LinagoraSysColors.material().onBackground, + filter: (room) => !room.isSpace && !room.isStoryRoom, + ), + selectedIcon: UnreadRoomsBadge( filter: (room) => !room.isSpace && !room.isStoryRoom, + isSelected: true, ), label: L10n.of(context)!.chats, ); case AdaptiveDestinationEnum.settings: return NavigationDestination( - icon: const TwakeNavigationIcon( - icon: Icons.settings, + icon: TwakeNavigationIcon( + color: LinagoraSysColors.material().onBackground, + icon: Icons.settings_outlined, + ), + selectedIcon: const TwakeNavigationIcon( + icon: Icons.settings_outlined, + isSelected: true, ), label: L10n.of(context)!.settings, ); @@ -41,4 +62,55 @@ enum AdaptiveDestinationEnum { ); } } + + BottomNavigationBarItem getNavigationDestinationForBottomBar( + BuildContext context, + ValueNotifier profile, + ) { + switch (this) { + case AdaptiveDestinationEnum.contacts: + return BottomNavigationBarItem( + icon: TwakeNavigationIcon( + color: LinagoraSysColors.material().tertiary, + icon: Icons.supervised_user_circle_outlined, + ), + label: L10n.of(context)!.contacts, + activeIcon: const TwakeNavigationIcon( + icon: Icons.supervised_user_circle_outlined, + isSelected: true, + ), + ); + case AdaptiveDestinationEnum.rooms: + return BottomNavigationBarItem( + icon: UnreadRoomsBadge( + color: LinagoraSysColors.material().tertiary, + filter: (room) => !room.isSpace && !room.isStoryRoom, + ), + activeIcon: UnreadRoomsBadge( + filter: (room) => !room.isSpace && !room.isStoryRoom, + isSelected: true, + ), + label: L10n.of(context)!.chats, + ); + case AdaptiveDestinationEnum.settings: + return BottomNavigationBarItem( + icon: BottomNavigationAvatar( + profile: profile, + isSelected: false, + ), + activeIcon: BottomNavigationAvatar( + profile: profile, + isSelected: true, + ), + label: L10n.of(context)!.settings, + ); + default: + return BottomNavigationBarItem( + icon: UnreadRoomsBadge( + filter: (room) => !room.isSpace && !room.isStoryRoom, + ), + label: L10n.of(context)!.chats, + ); + } + } } diff --git a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart index d1ab88c892..a48d80df0a 100644 --- a/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart +++ b/lib/widgets/mixins/handle_download_and_preview_file_mixin.dart @@ -62,8 +62,10 @@ mixin HandleDownloadAndPreviewFileMixin { builder: (context) { return PermissionDialog( permission: Permission.storage, - explainTextRequestPermission: - Text(L10n.of(context)!.explainStoragePermission), + explainTextRequestPermission: Text( + L10n.of(context)!.explainStoragePermission, + style: Theme.of(context).textTheme.bodyMedium, + ), icon: const Icon(Icons.preview_outlined), ); }, @@ -89,8 +91,10 @@ mixin HandleDownloadAndPreviewFileMixin { builder: (context) { return PermissionDialog( permission: Permission.storage, - explainTextRequestPermission: - Text(L10n.of(context)!.explainGoToStorageSetting), + explainTextRequestPermission: Text( + L10n.of(context)!.explainGoToStorageSetting, + style: Theme.of(context).textTheme.bodyMedium, + ), icon: const Icon(Icons.preview_outlined), ); }, diff --git a/lib/widgets/mxc_image.dart b/lib/widgets/mxc_image.dart index b805845e4f..0b8428fb50 100644 --- a/lib/widgets/mxc_image.dart +++ b/lib/widgets/mxc_image.dart @@ -38,6 +38,7 @@ class MxcImage extends StatefulWidget { final void Function()? onTapSelectMode; final ImageData? imageData; final bool isPreview; + final bool enableHeroAnimation; /// Enable it if the image is stretched, and you don't want to resize it final bool noResize; @@ -75,6 +76,7 @@ class MxcImage extends StatefulWidget { this.closeRightColumn, this.cacheWidth, this.cacheHeight, + this.enableHeroAnimation = true, super.key, }); @@ -182,26 +184,27 @@ class _MxcImageState extends State { } if (event != null) { - if (!PlatformInfos.isWeb) { - final fileInfo = await event.getFileInfo( - getThumbnail: widget.isThumbnail, - ); - if (fileInfo != null && fileInfo.filePath.isNotEmpty) { - setState(() { + try { + if (!PlatformInfos.isWeb) { + final fileInfo = await event.getFileInfo( + getThumbnail: widget.isThumbnail, + ); + if (fileInfo != null && fileInfo.filePath.isNotEmpty) { filePath = fileInfo.filePath; - }); - return; + return; + } } - } - final matrixFile = await event.downloadAndDecryptAttachment( - getThumbnail: widget.isThumbnail, - ); - if (!mounted) return; - setState(() { + final matrixFile = await event.downloadAndDecryptAttachment( + getThumbnail: widget.isThumbnail, + ); + if (!mounted) return; _imageData = matrixFile.bytes; - }); - return; + return; + } catch (e) { + Logs().e('MxcImage::Error while downloading image: $e'); + } + if (!mounted) return; } } @@ -271,7 +274,7 @@ class _MxcImageState extends State { ) : _buildImageWidget(); - if (widget.event?.eventId != null) { + if (widget.event?.eventId != null && widget.enableHeroAnimation) { imageWidget = Hero( tag: widget.event!.eventId, child: imageWidget, @@ -372,6 +375,7 @@ class _ImageWidget extends StatelessWidget { height: height, width: width, fit: BoxFit.cover, + errorBuilder: imageErrorWidgetBuilder, ) : Image.memory( data!, @@ -426,6 +430,7 @@ class _ImageNativeBuilder extends StatelessWidget { height: height, width: width, fit: BoxFit.cover, + errorBuilder: imageErrorWidgetBuilder, ); } return Image.file( diff --git a/lib/widgets/twake_app.dart b/lib/widgets/twake_app.dart index f998ad1de1..b3e3a9e175 100644 --- a/lib/widgets/twake_app.dart +++ b/lib/widgets/twake_app.dart @@ -44,6 +44,12 @@ class TwakeApp extends StatefulWidget { }, ); + static bool isCurrentPageIsInRooms() => + router.routeInformationProvider.value.uri.path.startsWith('/rooms/'); + + static bool isCurrentPageIsNotRooms() => + !router.routeInformationProvider.value.uri.path.startsWith('/rooms'); + @override TwakeAppState createState() => TwakeAppState(); } diff --git a/lib/widgets/twake_components/twake_header.dart b/lib/widgets/twake_components/twake_header.dart index e7ea46d8f0..bd5e275067 100644 --- a/lib/widgets/twake_components/twake_header.dart +++ b/lib/widgets/twake_components/twake_header.dart @@ -1,7 +1,7 @@ +import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; import 'package:fluffychat/presentation/model/chat_list/chat_selection_actions.dart'; -import 'package:fluffychat/utils/platform_infos.dart'; -import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/mixins/on_account_data_listen_mixin.dart'; import 'package:fluffychat/widgets/mixins/show_dialog_mixin.dart'; @@ -42,6 +42,8 @@ class _TwakeHeaderState extends State Profile(userId: ''), ); + static ResponsiveUtils responsive = getIt.get(); + void getCurrentProfile(Client client) async { currentProfileNotifier.value = Profile(userId: ''); final profile = await client.getProfileFromUserId( @@ -96,7 +98,9 @@ class _TwakeHeaderState extends State @override Widget build(BuildContext context) { return AppBar( - backgroundColor: LinagoraSysColors.material().onPrimary, + backgroundColor: responsive.isMobile(context) + ? LinagoraSysColors.material().background + : LinagoraSysColors.material().onPrimary, toolbarHeight: TwakeHeaderStyle.toolbarHeight, automaticallyImplyLeading: false, leadingWidth: TwakeHeaderStyle.leadingWidth, @@ -106,10 +110,15 @@ class _TwakeHeaderState extends State return Align( alignment: TwakeHeaderStyle.alignment, child: Row( + mainAxisAlignment: responsive.isMobile(context) + ? TwakeHeaderStyle.mobileTitleAllignement + : MainAxisAlignment.start, children: [ if (selectMode != SelectMode.select) ...[ Padding( - padding: TwakeHeaderStyle.paddingTitleHeader, + padding: responsive.isMobile(context) + ? EdgeInsets.zero + : TwakeHeaderStyle.paddingTitleHeader, child: Text( L10n.of(context)!.chats, style: Theme.of(context).textTheme.titleLarge?.copyWith( @@ -169,36 +178,6 @@ class _TwakeHeaderState extends State ), ), ], - Expanded( - flex: TwakeHeaderStyle.flexActions, - child: Padding( - padding: TwakeHeaderStyle.actionsPadding, - child: Align( - alignment: Alignment.centerRight, - child: PlatformInfos.isMobile - ? InkWell( - hoverColor: Colors.transparent, - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - onTap: widget.onClickAvatar, - child: ValueListenableBuilder( - valueListenable: currentProfileNotifier, - builder: (context, profile, _) { - return Avatar( - mxContent: profile.avatarUrl, - name: profile.displayName ?? - profile.userId.localpart, - size: TwakeHeaderStyle.avatarSize, - fontSize: - TwakeHeaderStyle.avatarFontSizeInAppBar, - ); - }, - ), - ) - : const SizedBox.shrink(), - ), - ), - ), ], ), ); diff --git a/lib/widgets/twake_components/twake_header_style.dart b/lib/widgets/twake_components/twake_header_style.dart index caf0da79f6..780bdd7f6a 100644 --- a/lib/widgets/twake_components/twake_header_style.dart +++ b/lib/widgets/twake_components/twake_header_style.dart @@ -20,6 +20,7 @@ class TwakeHeaderStyle { static bool isDesktop(BuildContext context) => responsive.isDesktop(context); static AlignmentGeometry alignment = AlignmentDirectional.centerStart; + static MainAxisAlignment mobileTitleAllignement = MainAxisAlignment.center; static const EdgeInsetsDirectional logoAppOfMultiplePadding = EdgeInsetsDirectional.all(16); diff --git a/lib/widgets/twake_components/twake_navigation_icon/twake_navigation_icon.dart b/lib/widgets/twake_components/twake_navigation_icon/twake_navigation_icon.dart index 8b0bc0582f..01f448e898 100644 --- a/lib/widgets/twake_components/twake_navigation_icon/twake_navigation_icon.dart +++ b/lib/widgets/twake_components/twake_navigation_icon/twake_navigation_icon.dart @@ -1,14 +1,19 @@ import 'package:fluffychat/widgets/twake_components/twake_navigation_icon/twake_navigation_icon_style.dart'; import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; class TwakeNavigationIcon extends StatelessWidget { final IconData icon; final int notificationCount; + final bool isSelected; + final Color? color; const TwakeNavigationIcon({ super.key, required this.icon, this.notificationCount = 0, + this.isSelected = false, + this.color, }); @override @@ -25,7 +30,9 @@ class TwakeNavigationIcon extends StatelessWidget { ), child: Icon( icon, - color: Theme.of(context).colorScheme.onSurfaceVariant, + color: isSelected + ? LinagoraSysColors.material().primary + : color ?? Theme.of(context).iconTheme.color, ), ); } diff --git a/lib/widgets/unread_rooms_badge.dart b/lib/widgets/unread_rooms_badge.dart index 4c691b9db8..752734f486 100644 --- a/lib/widgets/unread_rooms_badge.dart +++ b/lib/widgets/unread_rooms_badge.dart @@ -1,17 +1,19 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/widgets/twake_components/twake_navigation_icon/twake_navigation_icon.dart'; import 'package:flutter/material.dart'; - import 'package:matrix/matrix.dart'; - import 'matrix.dart'; class UnreadRoomsBadge extends StatelessWidget { final bool Function(Room) filter; + final bool isSelected; + final Color? color; const UnreadRoomsBadge({ super.key, required this.filter, + this.isSelected = false, + this.color, }); @override @@ -26,8 +28,10 @@ class UnreadRoomsBadge extends StatelessWidget { final unreadCount = getNotificationsCount(context); return TwakeNavigationIcon( - icon: Icons.chat, + icon: Icons.chat_bubble, notificationCount: unreadCount, + isSelected: isSelected, + color: color, ); }, ); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 090402fe16..f774eab097 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -52,6 +53,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_webrtc_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterWebRTCPlugin"); flutter_web_r_t_c_plugin_register_with_registrar(flutter_webrtc_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) handy_window_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin"); handy_window_plugin_register_with_registrar(handy_window_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index e59cbc86d0..fbbabf044e 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_avif_linux flutter_secure_storage_linux flutter_webrtc + gtk handy_window irondash_engine_context media_kit_libs_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 5a9d26a988..ae0532078f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import app_links import appkit_ui_element_colors import audio_session import connectivity_plus @@ -16,6 +17,7 @@ import emoji_picker_flutter import file_saver import file_selector_macos import firebase_core +import firebase_messaging import flutter_app_badger import flutter_avif_macos import flutter_image_compress_macos @@ -48,6 +50,7 @@ import wakelock_plus import window_to_front func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AppkitUiElementColorsPlugin.register(with: registry.registrar(forPlugin: "AppkitUiElementColorsPlugin")) AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) @@ -59,6 +62,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) FlutterAppBadgerPlugin.register(with: registry.registrar(forPlugin: "FlutterAppBadgerPlugin")) FlutterAvifPlugin.register(with: registry.registrar(forPlugin: "FlutterAvifPlugin")) FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 1e88dd260d..ccb36350d1 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -20,25 +20,43 @@ PODS: - FlutterMacOS - file_selector_macos (0.0.1): - FlutterMacOS - - Firebase/CoreOnly (9.6.0): - - FirebaseCore (= 9.6.0) - - firebase_core (1.24.0): - - Firebase/CoreOnly (~> 9.6.0) - - FlutterMacOS - - FirebaseCore (9.6.0): - - FirebaseCoreDiagnostics (~> 9.0) - - FirebaseCoreInternal (~> 9.0) - - GoogleUtilities/Environment (~> 7.7) - - GoogleUtilities/Logger (~> 7.7) - - FirebaseCoreDiagnostics (9.6.0): - - GoogleDataTransport (< 10.0.0, >= 9.1.4) - - GoogleUtilities/Environment (~> 7.7) - - GoogleUtilities/Logger (~> 7.7) - - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseCoreInternal (9.6.0): - - "GoogleUtilities/NSData+zlib (~> 7.7)" + - Firebase/CoreOnly (10.25.0): + - FirebaseCore (= 10.25.0) + - Firebase/Messaging (10.25.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 10.25.0) + - firebase_core (2.32.0): + - Firebase/CoreOnly (~> 10.25.0) + - FlutterMacOS + - firebase_messaging (14.9.4): + - Firebase/CoreOnly (~> 10.25.0) + - Firebase/Messaging (~> 10.25.0) + - firebase_core + - FlutterMacOS + - FirebaseCore (10.25.0): + - FirebaseCoreInternal (~> 10.0) + - GoogleUtilities/Environment (~> 7.12) + - GoogleUtilities/Logger (~> 7.12) + - FirebaseCoreInternal (10.29.0): + - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseInstallations (10.29.0): + - FirebaseCore (~> 10.0) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/UserDefaults (~> 7.8) + - PromisesObjC (~> 2.1) + - FirebaseMessaging (10.25.0): + - FirebaseCore (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleDataTransport (~> 9.3) + - GoogleUtilities/AppDelegateSwizzler (~> 7.8) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/Reachability (~> 7.8) + - GoogleUtilities/UserDefaults (~> 7.8) + - nanopb (< 2.30911.0, >= 2.30908.0) - flutter_app_badger (1.3.0): - FlutterMacOS + - flutter_avif_macos (0.0.1): + - FlutterMacOS - flutter_image_compress_macos (1.0.0): - FlutterMacOS - flutter_inappwebview_macos (0.0.1): @@ -61,15 +79,31 @@ PODS: - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/AppDelegateSwizzler (7.13.3): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy - GoogleUtilities/Environment (7.13.3): - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Privacy + - GoogleUtilities/Network (7.13.3): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability - "GoogleUtilities/NSData+zlib (7.13.3)": - GoogleUtilities/Privacy - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/Reachability (7.13.3): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (7.13.3): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy - irondash_engine_context (0.0.1): - FlutterMacOS - just_audio (0.0.1): @@ -84,11 +118,11 @@ PODS: - FlutterMacOS - media_kit_video (0.0.1): - FlutterMacOS - - nanopb (2.30909.1): - - nanopb/decode (= 2.30909.1) - - nanopb/encode (= 2.30909.1) - - nanopb/decode (2.30909.1) - - nanopb/encode (2.30909.1) + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) - OrderedSet (5.0.0) - package_info_plus (0.0.1): - FlutterMacOS @@ -141,7 +175,9 @@ DEPENDENCIES: - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) + - firebase_messaging (from `Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos`) - flutter_app_badger (from `Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos`) + - flutter_avif_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_avif_macos/macos`) - flutter_image_compress_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos`) - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) @@ -177,8 +213,9 @@ SPEC REPOS: trunk: - Firebase - FirebaseCore - - FirebaseCoreDiagnostics - FirebaseCoreInternal + - FirebaseInstallations + - FirebaseMessaging - GoogleDataTransport - GoogleUtilities - nanopb @@ -210,8 +247,12 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos firebase_core: :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos + firebase_messaging: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_messaging/macos flutter_app_badger: :path: Flutter/ephemeral/.symlinks/plugins/flutter_app_badger/macos + flutter_avif_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_avif_macos/macos flutter_image_compress_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_image_compress_macos/macos flutter_inappwebview_macos: @@ -284,12 +325,15 @@ SPEC CHECKSUMS: emoji_picker_flutter: 533634326b1c5de9a181ba14b9758e6dfe967a20 file_saver: 44e6fbf666677faf097302460e214e977fdd977b file_selector_macos: 54fdab7caa3ac3fc43c9fac4d7d8d231277f8cf2 - Firebase: 5ae8b7cf8efce559a653aef0ad95bab3f427c351 - firebase_core: 970bc7db019f0985976324d90cdc370527c31461 - FirebaseCore: 2082fffcd855f95f883c0a1641133eb9bbe76d40 - FirebaseCoreDiagnostics: 99a495094b10a57eeb3ae8efa1665700ad0bdaa6 - FirebaseCoreInternal: bca76517fe1ed381e989f5e7d8abb0da8d85bed3 + Firebase: 0312a2352584f782ea56f66d91606891d4607f06 + firebase_core: b5b8b60dad71f93132bbaa21e8d1379367d824f0 + firebase_messaging: d821ad7103878837085612e389fe66203c5c6c0c + FirebaseCore: 7ec4d0484817f12c3373955bc87762d96842d483 + FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 + FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd + FirebaseMessaging: 88950ba9485052891ebe26f6c43a52bb62248952 flutter_app_badger: 55a64b179f8438e89d574320c77b306e327a1730 + flutter_avif_macos: 065347857175874d00f4736233a6ae0fc2b66f0a flutter_image_compress_macos: c26c3c13ea0f28ae6dea4e139b3292e7729f99f1 flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 @@ -307,7 +351,7 @@ SPEC CHECKSUMS: media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 - nanopb: d4d75c12cd1316f4a64e3c6963f879ecd4b5e0d5 + nanopb: 438bc412db1928dac798aa6fd75726007be04262 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 9f7368bd71..66c697ac00 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -184,6 +184,7 @@ 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, 28EE3A13194E86CCA3B8236A /* [CP] Embed Pods Frameworks */, + 6CEE6CE5DB9536C808CAEA5B /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -261,8 +262,9 @@ inputPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${BUILT_PRODUCTS_DIR}/FirebaseCore/FirebaseCore.framework", - "${BUILT_PRODUCTS_DIR}/FirebaseCoreDiagnostics/FirebaseCoreDiagnostics.framework", "${BUILT_PRODUCTS_DIR}/FirebaseCoreInternal/FirebaseCoreInternal.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseInstallations/FirebaseInstallations.framework", + "${BUILT_PRODUCTS_DIR}/FirebaseMessaging/FirebaseMessaging.framework", "${BUILT_PRODUCTS_DIR}/GoogleDataTransport/GoogleDataTransport.framework", "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/OrderedSet/OrderedSet.framework", @@ -279,6 +281,7 @@ "${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework", "${BUILT_PRODUCTS_DIR}/file_selector_macos/file_selector_macos.framework", "${BUILT_PRODUCTS_DIR}/flutter_app_badger/flutter_app_badger.framework", + "${BUILT_PRODUCTS_DIR}/flutter_avif_macos/flutter_avif_macos.framework", "${BUILT_PRODUCTS_DIR}/flutter_image_compress_macos/flutter_image_compress_macos.framework", "${BUILT_PRODUCTS_DIR}/flutter_inappwebview_macos/flutter_inappwebview_macos.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", @@ -332,8 +335,9 @@ name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCore.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreDiagnostics.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseCoreInternal.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseInstallations.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FirebaseMessaging.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleDataTransport.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OrderedSet.framework", @@ -350,6 +354,7 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_selector_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_app_badger.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_avif_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_image_compress_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_inappwebview_macos.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", @@ -443,6 +448,24 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh\ntouch Flutter/ephemeral/tripwire\n"; }; + 6CEE6CE5DB9536C808CAEA5B /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/firebase_messaging/firebase_messaging_Privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/firebase_messaging_Privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; E26D3B733E5CAE51F86A6BE1 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/pubspec.lock b/pubspec.lock index 07efc7182e..752e55431f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "67.0.0" + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7" + url: "https://pub.dev" + source: hosted + version: "1.3.35" adaptive_dialog: dependency: "direct main" description: @@ -49,6 +57,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "4acba851087b25136e8f6e32a53bd4536eb3bec69947ddb66e7b9a5792ceb0c7" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" appkit_ui_element_colors: dependency: transitive description: @@ -165,10 +205,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa" + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.4.12" build_runner_core: dependency: transitive description: @@ -220,11 +260,12 @@ packages: callkeep: dependency: "direct main" description: - name: callkeep - sha256: "9e86e9632a603a61f7045c179ea5ca0ee4da0a49fc5f80c2fe09fb422b96d3c6" - url: "https://pub.dev" - source: hosted - version: "0.3.3" + path: "." + ref: master + resolved-ref: a3c46737b4467898321c51affbbc03a153169d40 + url: "https://github.com/flutter-webrtc/callkeep" + source: git + version: "0.4.0" camera: dependency: transitive description: @@ -366,7 +407,7 @@ packages: description: path: "." ref: master - resolved-ref: "083024c6200f4773e34c471b4001cd76fe1c9cec" + resolved-ref: a26353c9d8ce23321f11ed9d3386844dc0b0df65 url: "git@github.com:linagora/flutter_contacts.git" source: git version: "0.6.3" @@ -605,10 +646,11 @@ packages: fcm_shared_isolate: dependency: "direct main" description: - name: fcm_shared_isolate - sha256: "45c66353aad6a237437b3d071bddddd35d391b75c3e06aaec535a9df32d44dbe" - url: "https://pub.dev" - source: hosted + path: "." + ref: main + resolved-ref: dd834a577372b74df6d95b9a08d30e38a2d64bbc + url: "https://github.com/famedly/fcm_shared_isolate.git" + source: git version: "0.1.0" ffi: dependency: "direct overridden" @@ -710,26 +752,50 @@ packages: dependency: transitive description: name: firebase_core - sha256: "4f1d7c13a909e82ff026679c9b8493cdeb35a9c76bc46c42bf9e2240c9e57e80" + sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c" url: "https://pub.dev" source: hosted - version: "1.24.0" + version: "2.32.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 + sha256: "3c3a1e92d6f4916c32deea79c4a7587aa0e9dbbe5889c7a16afcf005a485ee02" url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "5.2.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "839f1b48032a61962792cea1225fae030d4f27163867f181d6d2072dd40acbee" + sha256: e8d1e22de72cb21cdcfc5eed7acddab3e99cd83f3b317f54f7a96c32f25fd11e + url: "https://pub.dev" + source: hosted + version: "2.17.4" + firebase_messaging: + dependency: transitive + description: + name: firebase_messaging + sha256: a1662cc95d9750a324ad9df349b873360af6f11414902021f130c68ec02267c4 url: "https://pub.dev" source: hosted - version: "1.7.3" + version: "14.9.4" + firebase_messaging_platform_interface: + dependency: transitive + description: + name: firebase_messaging_platform_interface + sha256: "87c4a922cb6f811cfb7a889bdbb3622702443c52a0271636cbc90d813ceac147" + url: "https://pub.dev" + source: hosted + version: "4.5.37" + firebase_messaging_web: + dependency: transitive + description: + name: firebase_messaging_web + sha256: "0d34dca01a7b103ed7f20138bffbb28eb0e61a677bf9e78a028a932e2c7322d5" + url: "https://pub.dev" + source: hosted + version: "3.8.7" fixnum: dependency: transitive description: @@ -754,11 +820,12 @@ packages: flutter_app_badger: dependency: "direct main" description: - name: flutter_app_badger - sha256: "64d4a279bab862ed28850431b9b446b9820aaae0bf363322d51077419f930fa8" - url: "https://pub.dev" - source: hosted - version: "1.5.0" + path: "." + ref: master + resolved-ref: "0cc96b63db11b3a3615d2a798022fd35511fa4ac" + url: "https://github.com/bitsydarel/flutter_app_badger.git" + source: git + version: "1.5.2" flutter_app_lock: dependency: "direct main" description: @@ -1048,26 +1115,26 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef" + sha256: c500d5d9e7e553f06b61877ca6b9c8b92c570a4c8db371038702e8ce57f8a50f url: "https://pub.dev" source: hosted - version: "17.1.2" + version: "17.2.2" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af url: "https://pub.dev" source: hosted - version: "4.0.0+1" + version: "4.0.1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.2.0" flutter_localizations: dependency: "direct main" description: flutter @@ -1150,10 +1217,10 @@ packages: dependency: "direct main" description: name: flutter_ringtone_player - sha256: "0b036416fda0654da52221989bd1a8ccd2876cea57f61ecc3a4fc272bd738c67" + sha256: d0277a04e629a6582d776f5dcc2a879a733f7326ba073b872a9ccfbff9d9b51f url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0+3" flutter_rust_bridge: dependency: transitive description: @@ -1311,18 +1378,18 @@ packages: dependency: "direct overridden" description: name: geolocator_android - sha256: a4834a98fab5124f2d5b881e62a40ebb4a71d6aad6ad577e047a3ffb69b67dac - url: "https://hanntech-gmbh.gitlab.io/free2pass/flutter-geolocator-floss/" + sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47" + url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "4.6.1" geolocator_platform_interface: dependency: transitive description: name: geolocator_platform_interface - sha256: "9d6f34a8a4b704d504f34acc5e52d880a7d2caedd99739902d6319179b0336d4" + sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "4.2.4" get_it: dependency: "direct main" description: @@ -1371,6 +1438,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" handy_window: dependency: "direct main" description: @@ -1379,6 +1454,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.1.9" + heif_converter: + dependency: "direct main" + description: + name: heif_converter + sha256: "63ffc2a72026942de3fcb61450da197100759936085c8279aef35b995b3c1bb3" + url: "https://pub.dev" + source: hosted + version: "1.0.0" highlight: dependency: transitive description: @@ -1462,10 +1545,11 @@ packages: image_gallery_saver: dependency: "direct main" description: - name: image_gallery_saver - sha256: "0aba74216a4d9b0561510cb968015d56b701ba1bd94aace26aacdd8ae5761816" - url: "https://pub.dev" - source: hosted + path: "." + ref: master + resolved-ref: "47f36a58a1e3cecf988dc83dfd10f7a8a5941e02" + url: "https://github.com/FlutterStudioIst/image_gallery_saver.git" + source: git version: "2.0.3" image_picker: dependency: "direct main" @@ -1678,18 +1762,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -1703,7 +1787,7 @@ packages: description: path: "." ref: master - resolved-ref: ffbcb9dd4d6cefb7fb60b7a9b15ad684dae6bdff + resolved-ref: "0f666ca14fd4f0710775ca3de03cde73b21de7d6" url: "git@github.com:linagora/linagora-design-flutter.git" source: git version: "0.0.1" @@ -1723,6 +1807,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + logger: + dependency: transitive + description: + name: logger + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" + url: "https://pub.dev" + source: hosted + version: "2.4.0" logging: dependency: transitive description: @@ -1775,10 +1867,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" matrix: dependency: "direct main" description: @@ -1890,10 +1982,10 @@ packages: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mgrs_dart: dependency: transitive description: @@ -1937,10 +2029,11 @@ packages: native_imaging: dependency: "direct main" description: - name: native_imaging - sha256: "182ccd8e0815a8a2158500ef66c828c030f6b9e05783e41e22f33bbcfd46a3d5" - url: "https://pub.dev" - source: hosted + path: "." + ref: main + resolved-ref: d76335e2039c041585df8103f5d4f5924e9e2add + url: "https://github.com/famedly/dart_native_imaging" + source: git version: "0.1.1" nested: dependency: transitive @@ -2162,10 +2255,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" platform_detect: dependency: transitive description: @@ -2314,10 +2407,10 @@ packages: dependency: transitive description: name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" random_string: dependency: transitive description: @@ -2330,9 +2423,9 @@ packages: dependency: "direct main" description: path: "." - ref: "receive-.txt-v2" - resolved-ref: "7ac2cccb3e1ac4bddce7e287adcb69726f368b89" - url: "git@github.com:krabbenprgr/receive_sharing_intent.git" + ref: master + resolved-ref: f23f7fb0fad25ae80a88350ad8654851f1ee682f + url: "https://github.com/linagora/receive_sharing_intent.git" source: git version: "1.4.5" record: @@ -2761,10 +2854,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" timezone: dependency: transitive description: @@ -2821,30 +2914,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" - uni_links: - dependency: "direct main" - description: - name: uni_links - sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" - url: "https://pub.dev" - source: hosted - version: "0.5.1" - uni_links_platform_interface: - dependency: transitive - description: - name: uni_links_platform_interface - sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - uni_links_web: - dependency: transitive - description: - name: uni_links_web - sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" - url: "https://pub.dev" - source: hosted - version: "0.1.0" unicode: dependency: transitive description: @@ -3133,10 +3202,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.4" volume_controller: dependency: transitive description: @@ -3315,5 +3384,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" + dart: ">=3.5.0-259.0.dev <4.0.0" flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index e43ec65d12..69e389d97c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fluffychat description: A convenient Matrix-based tool for personal and corporate communication. publish_to: none -version: 2.5.8+2330 +version: 2.6.3+2330 environment: sdk: ">=3.1.3 <4.0.0" @@ -16,8 +16,8 @@ dependencies: receive_sharing_intent: git: - url: git@github.com:krabbenprgr/receive_sharing_intent.git - ref: receive-.txt-v2 + url: https://github.com/linagora/receive_sharing_intent.git + ref: master # TODO: Android native lib build error: https://github.com/jonataslaw/VideoCompress/issues/240 # video_compress: ^3.1.1 @@ -64,7 +64,11 @@ dependencies: flutter_adaptive_scaffold: ^0.1.4 animations: ^2.0.7 blurhash_dart: ^1.1.0 - callkeep: ^0.3.2 + # TODO: remove it after it release a new version, the PR is merged to master + callkeep: + git: + url: https://github.com/flutter-webrtc/callkeep + ref: master chewie: ^1.3.6 collection: ^1.16.0 connectivity_plus: ^3.0.2 @@ -77,23 +81,31 @@ dependencies: emoji_picker_flutter: ^1.5.1 emoji_proposal: ^0.0.1 emojis: ^0.9.9 - fcm_shared_isolate: ^0.1.0 + # TODO: remove this after this PR is merged https://gitlab.com/famedly/company/frontend/libraries/native_imaging/-/merge_requests/22 + fcm_shared_isolate: + git: + url: https://github.com/famedly/fcm_shared_isolate.git + ref: main file_picker: ^8.0.5 flutter: sdk: flutter - flutter_app_badger: ^1.5.0 + # TODO: remove this after this PR is merged https://github.com/g123k/flutter_app_badger/pull/92 + flutter_app_badger: + git: + url: https://github.com/bitsydarel/flutter_app_badger.git + ref: master flutter_app_lock: ^3.0.0 flutter_blurhash: ^0.8.2 flutter_cache_manager: ^3.3.0 flutter_foreground_task: ^3.10.0 - flutter_local_notifications: ^17.1.2 + flutter_local_notifications: 17.2.2 flutter_localizations: sdk: flutter flutter_map: ^4.0.0 # flutter_matrix_html: ^1.1.0 flutter_olm: ^1.2.0 flutter_openssl_crypto: ^0.3.0 - flutter_ringtone_player: ^3.1.1 + flutter_ringtone_player: 4.0.0+3 flutter_secure_storage: ^9.2.1 flutter_svg: ^0.22.0 flutter_typeahead: ^5.1.0 @@ -114,7 +126,11 @@ dependencies: latlong2: ^0.8.1 matrix_homeserver_recommendations: ^0.3.0 matrix_link_text: ^2.0.0 - native_imaging: ^0.1.0 + # TODO: remove this after this PR is merged https://gitlab.com/famedly/company/frontend/libraries/native_imaging/-/merge_requests/22 + native_imaging: + git: + url: https://github.com/famedly/dart_native_imaging + ref: main package_info_plus: ^8.0.0 path_provider: ^2.0.15 permission_handler: ^11.0.1 @@ -134,8 +150,8 @@ dependencies: url: https://github.com/alirezat66/skeletons.git ref: master tor_detector_web: ^1.1.0 - uni_links: ^0.5.1 - unifiedpush: ^5.0.1 + app_links: ^6.2.0 + unifiedpush: ^5.0.1 universal_html: ^2.0.8 url_launcher: ^6.0.20 vibration: ^1.7.4-nullsafety.0 @@ -163,7 +179,11 @@ dependencies: async: ^2.11.0 cached_network_image: ^3.2.3 flutter_image_compress: ^2.0.4 - image_gallery_saver: ^2.0.3 + # TODO: remove when https://github.com/hui-z/image_gallery_saver/pull/310 is merged + image_gallery_saver: + git: + url: https://github.com/FlutterStudioIst/image_gallery_saver.git + ref: master file_saver: ^0.2.12 flutter_keyboard_visibility: ^6.0.0 media_kit: ^1.1.7 @@ -185,9 +205,10 @@ dependencies: gal: 2.3.0 auto_size_text: 3.0.0 flutter_avif: 2.4.1 + heif_converter: 1.0.0 dev_dependencies: - build_runner: ^2.3.3 + build_runner: 2.4.12 flutter_launcher_icons: ^0.13.1 flutter_lints: ^3.0.2 flutter_native_splash: ^2.0.3+1 @@ -257,10 +278,7 @@ dependency_overrides: git: url: https://gitlab.com/TheOneWithTheBraid/flutter_secure_storage_windows.git ref: main - geolocator_android: - hosted: - name: geolocator_android - url: https://hanntech-gmbh.gitlab.io/free2pass/flutter-geolocator-floss + geolocator_android: 4.6.1 # waiting for null safety # Upstream pull request: https://github.com/AntoineMarcel/keyboard_shortcuts/pull/13 keyboard_shortcuts: diff --git a/test/mixin/contacts_view_controller_mixin_test.dart b/test/mixin/contacts_view_controller_mixin_test.dart index 6b12c652ae..6dfa7d9f2f 100644 --- a/test/mixin/contacts_view_controller_mixin_test.dart +++ b/test/mixin/contacts_view_controller_mixin_test.dart @@ -33,6 +33,7 @@ class ConcretePresentationSearch extends PresentationSearch { } @GenerateNiceMocks([ + MockSpec(), MockSpec(), MockSpec(), MockSpec(), @@ -114,11 +115,13 @@ void main() { late MockContactsViewControllerMixin mockContactsViewControllerMixin; late Client mockClient; late MatrixLocalizations mockMatrixLocalizations; + late BuildContext mockBuildContext; setUp(() { mockContactsViewControllerMixin = MockContactsViewControllerMixin(); mockMatrixLocalizations = MockMatrixLocalizations(); mockClient = MockClient(); + mockBuildContext = MockBuildContext(); }); group('Test ContactsViewControllerMixin on Web', () { @@ -157,12 +160,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -230,12 +235,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -305,12 +312,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -396,12 +405,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -493,12 +504,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -663,12 +676,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -833,12 +848,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1003,12 +1020,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1202,12 +1221,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1469,12 +1490,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1542,12 +1565,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1619,12 +1644,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1722,12 +1749,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1819,12 +1848,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -1989,12 +2020,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -2161,12 +2194,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -2333,12 +2368,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), @@ -2534,12 +2571,14 @@ void main() { ); mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ); verify( mockContactsViewControllerMixin.initialFetchContacts( + context: mockBuildContext, client: mockClient, matrixLocalizations: mockMatrixLocalizations, ), diff --git a/test/pages/new_private_chat/widget/contact_status_widget_test.dart b/test/pages/new_private_chat/widget/contact_status_widget_test.dart new file mode 100644 index 0000000000..f7ea163077 --- /dev/null +++ b/test/pages/new_private_chat/widget/contact_status_widget_test.dart @@ -0,0 +1,64 @@ +import 'package:fluffychat/pages/new_private_chat/widget/contact_status_widget.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:fluffychat/domain/model/contact/contact_status.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; + +void main() { + group('ContactStatusWidget', () { + testWidgets('renders correctly for inactive status', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: L10n.localizationsDelegates, + supportedLocales: L10n.supportedLocales, + home: Scaffold( + body: ContactStatusWidget(status: ContactStatus.inactive), + ), + ), + ); + + expect(find.byType(SvgPicture), findsOneWidget); + + expect(find.byType(Text), findsOneWidget); + + final svgPicture = tester.widget(find.byType(SvgPicture)); + expect( + svgPicture.colorFilter, + ColorFilter.mode( + LinagoraRefColors.material().neutral[60]!, + BlendMode.srcIn, + ), + ); + + final text = tester.widget(find.byType(Text)); + expect(text.style?.color, LinagoraRefColors.material().neutral[60]); + expect( + text.style?.fontSize, + Theme.of(tester.element(find.byType(ContactStatusWidget))) + .textTheme + .bodySmall + ?.fontSize, + ); + }); + + testWidgets('renders correctly for active status', + (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: L10n.localizationsDelegates, + supportedLocales: L10n.supportedLocales, + home: Scaffold( + body: ContactStatusWidget(status: ContactStatus.active), + ), + ), + ); + + expect(find.byType(SvgPicture), findsNothing); + expect(find.byType(Text), findsNothing); + expect(find.byType(SizedBox), findsOneWidget); + }); + }); +} diff --git a/test/utils/date_time_extension_test.dart b/test/utils/date_time_extension_test.dart index 6c449a2483..017eb1f612 100644 --- a/test/utils/date_time_extension_test.dart +++ b/test/utils/date_time_extension_test.dart @@ -64,7 +64,7 @@ void main() async { testWidgets( 'GIVEN the date time to display is Monday of current week\n' 'THEN should display the Monday\n', (WidgetTester tester) async { - const expectedDisplayText = 'Monday'; + const expectedDisplayText = 'Mon'; final currentTime = DateTime(2022, 1, 1); final timeToTest = DateTime(2021, 12, 27, 12, 5); @@ -105,7 +105,7 @@ void main() async { testWidgets( 'GIVEN the date time to display is Tuesday of current week\n' 'THEN should display Tuesday\n', (WidgetTester tester) async { - const expectedDisplayText = 'Tuesday'; + const expectedDisplayText = 'Tue'; final currentTime = DateTime(2022, 1, 1); final timeToTest = DateTime(2021, 12, 28, 12, 5); @@ -146,7 +146,7 @@ void main() async { testWidgets( 'GIVEN the date time to display is Wednesday of current week\n' 'THEN should display Wednesday\n', (WidgetTester tester) async { - const expectedDisplayText = 'Wednesday'; + const expectedDisplayText = 'Wed'; final currentTime = DateTime(2022, 1, 1); final timeToTest = DateTime(2021, 12, 29, 12, 5); @@ -187,7 +187,7 @@ void main() async { testWidgets( 'GIVEN the date time to display is Thursday of current week\n' 'THEN should display Thursday\n', (WidgetTester tester) async { - const expectedDisplayText = 'Thursday'; + const expectedDisplayText = 'Thu'; final currentTime = DateTime(2022, 1, 1); final timeToTest = DateTime(2021, 12, 30, 12, 5); @@ -228,7 +228,7 @@ void main() async { testWidgets( 'GIVEN the date time to display is Friday of current week\n' 'THEN should display Friday\n', (WidgetTester tester) async { - const expectedDisplayText = 'Friday'; + const expectedDisplayText = 'Fri'; final currentTime = DateTime(2022, 1, 1); final timeToTest = DateTime(2021, 12, 31, 12, 5); @@ -269,7 +269,7 @@ void main() async { testWidgets( 'GIVEN the date time to display is Saturday of current week\n' 'THEN should display Saturday\n', (WidgetTester tester) async { - const expectedDisplayText = 'Saturday'; + const expectedDisplayText = 'Sat'; final currentTime = DateTime(2024, 2, 25); final timeToTest = DateTime(2024, 2, 24, 12, 5); @@ -310,7 +310,7 @@ void main() async { testWidgets( 'GIVEN the date time to display is Sunday of current week\n' 'THEN should display Sunday\n', (WidgetTester tester) async { - const expectedDisplayText = 'Sunday'; + const expectedDisplayText = 'Sun'; final currentTime = DateTime(2022, 1, 1); final timeToTest = DateTime(2022, 1, 2, 12, 5); @@ -352,7 +352,7 @@ void main() async { 'GIVEN the current time is Sunday\n' 'AND the date time to display is Friday of current week\n' 'THEN should display Friday\n', (WidgetTester tester) async { - const expectedDisplayText = 'Friday'; + const expectedDisplayText = 'Fri'; final currentTime = DateTime(2024, 3, 3); final timeToTest = DateTime(2024, 3, 1, 12, 5); @@ -394,7 +394,7 @@ void main() async { 'GIVEN the current time is Sunday\n' 'AND the date time to display is Saturday of current week\n' 'THEN should display Saturday\n', (WidgetTester tester) async { - const expectedDisplayText = 'Saturday'; + const expectedDisplayText = 'Sat'; final currentTime = DateTime(2024, 3, 3); final timeToTest = DateTime(2024, 3, 2, 12, 5); diff --git a/test/utils/string_extension_test.dart b/test/utils/string_extension_test.dart index d37b6f8164..96fcbda0c9 100644 --- a/test/utils/string_extension_test.dart +++ b/test/utils/string_extension_test.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/utils/string_extension.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -377,4 +378,225 @@ void main() { expect(''.extractInnerText(), isEmpty); }); }); + + group('buildHighlightTextSpans tests', () { + test( + 'buildHighlightTextSpans handles special * and \\ characters in highlightText', + () { + const text = + 'This is a test string with special characters like * and \\.'; + const highlightText = '* and \\'; + final expectedSpans = [ + const TextSpan( + text: 'This is a test string with special characters like ', + style: TextStyle(color: Colors.black), + ), + const TextSpan(text: '* and \\', style: TextStyle(color: Colors.red)), + const TextSpan(text: '.', style: TextStyle(color: Colors.black)), + ]; + + final result = text.buildHighlightTextSpans( + highlightText, + style: const TextStyle(color: Colors.black), + highlightStyle: const TextStyle(color: Colors.red), + ); + + expect(result.length, expectedSpans.length); + + for (int i = 0; i < result.length; i++) { + expect(result[i].text, expectedSpans[i].text); + expect(result[i].style, expectedSpans[i].style); + } + }); + + test( + 'buildHighlightTextSpans handles special \\ characters in highlightText', + () { + const text = 'This is a test string with special characters like 123\\.'; + const highlightText = '123'; + final expectedSpans = [ + const TextSpan( + text: 'This is a test string with special characters like ', + style: TextStyle(color: Colors.black), + ), + const TextSpan(text: '123', style: TextStyle(color: Colors.red)), + const TextSpan(text: '\\.', style: TextStyle(color: Colors.black)), + ]; + + final result = text.buildHighlightTextSpans( + highlightText, + style: const TextStyle(color: Colors.black), + highlightStyle: const TextStyle(color: Colors.red), + ); + + expect(result.length, expectedSpans.length); + + for (int i = 0; i < result.length; i++) { + expect(result[i].text, expectedSpans[i].text); + expect(result[i].style, expectedSpans[i].style); + } + }); + + test('buildHighlightTextSpans handles special characters in highlightText', + () { + const text = 'This is a test string with special characters like 123.'; + const highlightText = '123'; + final expectedSpans = [ + const TextSpan( + text: 'This is a test string with special characters like ', + style: TextStyle(color: Colors.black), + ), + const TextSpan(text: '123', style: TextStyle(color: Colors.red)), + const TextSpan(text: '.', style: TextStyle(color: Colors.black)), + ]; + + final result = text.buildHighlightTextSpans( + highlightText, + style: const TextStyle(color: Colors.black), + highlightStyle: const TextStyle(color: Colors.red), + ); + + expect(result.length, expectedSpans.length); + + for (int i = 0; i < result.length; i++) { + expect(result[i].text, expectedSpans[i].text); + expect(result[i].style, expectedSpans[i].style); + } + }); + + test('buildHighlightTextSpans handles special characters in highlightText', + () { + const text = 'This is a test string with special characters like 123.'; + const highlightText = '123.'; + final expectedSpans = [ + const TextSpan( + text: 'This is a test string with special characters like ', + style: TextStyle(color: Colors.black), + ), + const TextSpan(text: '123.', style: TextStyle(color: Colors.red)), + const TextSpan(text: '', style: TextStyle(color: Colors.black)), + ]; + + final result = text.buildHighlightTextSpans( + highlightText, + style: const TextStyle(color: Colors.black), + highlightStyle: const TextStyle(color: Colors.red), + ); + + expect(result.length, expectedSpans.length); + + for (int i = 0; i < result.length; i++) { + expect(result[i].text, expectedSpans[i].text); + expect(result[i].style, expectedSpans[i].style); + } + }); + + test('buildHighlightTextSpans handles special characters in highlightText', + () { + const text = 'This is a test string with special characters like 123\\'; + const highlightText = '123\\'; + final expectedSpans = [ + const TextSpan( + text: 'This is a test string with special characters like ', + style: TextStyle(color: Colors.black), + ), + const TextSpan(text: '123\\', style: TextStyle(color: Colors.red)), + const TextSpan(text: '', style: TextStyle(color: Colors.black)), + ]; + + final result = text.buildHighlightTextSpans( + highlightText, + style: const TextStyle(color: Colors.black), + highlightStyle: const TextStyle(color: Colors.red), + ); + + expect(result.length, expectedSpans.length); + + for (int i = 0; i < result.length; i++) { + expect(result[i].text, expectedSpans[i].text); + expect(result[i].style, expectedSpans[i].style); + } + }); + + test('buildHighlightTextSpans handles special characters in highlightText', + () { + const text = 'This is a test string with special characters like 123@@++'; + const highlightText = '123@@++'; + final expectedSpans = [ + const TextSpan( + text: 'This is a test string with special characters like ', + style: TextStyle(color: Colors.black), + ), + const TextSpan(text: '123@@++', style: TextStyle(color: Colors.red)), + const TextSpan(text: '', style: TextStyle(color: Colors.black)), + ]; + + final result = text.buildHighlightTextSpans( + highlightText, + style: const TextStyle(color: Colors.black), + highlightStyle: const TextStyle(color: Colors.red), + ); + + expect(result.length, expectedSpans.length); + + for (int i = 0; i < result.length; i++) { + expect(result[i].text, expectedSpans[i].text); + expect(result[i].style, expectedSpans[i].style); + } + }); + + test('buildHighlightTextSpans handles special characters in highlightText', + () { + const text = 'This is a test string with special characters like .123'; + const highlightText = '.123'; + final expectedSpans = [ + const TextSpan( + text: 'This is a test string with special characters like ', + style: TextStyle(color: Colors.black), + ), + const TextSpan(text: '.123', style: TextStyle(color: Colors.red)), + const TextSpan(text: '', style: TextStyle(color: Colors.black)), + ]; + + final result = text.buildHighlightTextSpans( + highlightText, + style: const TextStyle(color: Colors.black), + highlightStyle: const TextStyle(color: Colors.red), + ); + + expect(result.length, expectedSpans.length); + + for (int i = 0; i < result.length; i++) { + expect(result[i].text, expectedSpans[i].text); + expect(result[i].style, expectedSpans[i].style); + } + }); + + test('buildHighlightTextSpans handles special characters in highlightText', + () { + const text = 'This is a test string with special characters like \\123'; + const highlightText = '\\123'; + final expectedSpans = [ + const TextSpan( + text: 'This is a test string with special characters like ', + style: TextStyle(color: Colors.black), + ), + const TextSpan(text: '\\123', style: TextStyle(color: Colors.red)), + const TextSpan(text: '', style: TextStyle(color: Colors.black)), + ]; + + final result = text.buildHighlightTextSpans( + highlightText, + style: const TextStyle(color: Colors.black), + highlightStyle: const TextStyle(color: Colors.red), + ); + + expect(result.length, expectedSpans.length); + + for (int i = 0; i < result.length; i++) { + expect(result[i].text, expectedSpans[i].text); + expect(result[i].style, expectedSpans[i].style); + } + }); + }); } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index fd7b1097da..758a8ece95 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -28,6 +30,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); DesktopDropPluginRegisterWithRegistrar( @@ -42,6 +46,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSaverPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); FlutterAvifWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterAvifWindowsPlugin")); FlutterWebRTCPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index c03bed2529..13b0d4c970 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links connectivity_plus desktop_drop desktop_lifecycle @@ -10,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST emoji_picker_flutter file_saver file_selector_windows + firebase_core flutter_avif_windows flutter_webrtc gal