diff --git a/.github/workflows/gh-pages.yaml b/.github/workflows/gh-pages.yaml index 4dc57f5ee2..c49ade49c7 100644 --- a/.github/workflows/gh-pages.yaml +++ b/.github/workflows/gh-pages.yaml @@ -11,6 +11,8 @@ jobs: deploy_preview: name: Deploy preview versions on pull requests runs-on: ubuntu-latest + concurrency: + group: ${{ github.workflow }}-${{ github.ref }} permissions: deployments: write pull-requests: write diff --git a/.github/workflows/image.yaml b/.github/workflows/image.yaml index 46fef09072..d9764b4511 100644 --- a/.github/workflows/image.yaml +++ b/.github/workflows/image.yaml @@ -20,6 +20,9 @@ jobs: environment: dev steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -54,6 +57,7 @@ jobs: uses: docker/build-push-action@v4 with: push: true + platforms: "linux/amd64,linux/arm64" cache-from: | type=gha cache-to: | @@ -70,6 +74,9 @@ jobs: environment: prod steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 @@ -105,6 +112,7 @@ jobs: uses: docker/build-push-action@v4 with: push: true + platforms: "linux/amd64,linux/arm64" cache-from: | type=gha cache-to: | diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index ddd5d18125..4c9a2d845e 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -149,7 +149,7 @@ jobs: run: | case "$RUNNER_OS" in Linux) - cp dist/*/* "twake-$GITHUB_REF_NAME-linux-amd64.AppImage";; + cp dist/* "twake-$GITHUB_REF_NAME-linux-amd64.AppImage";; macOS) cp dist/*/* "twake-$GITHUB_REF_NAME-macos-amd64.dmg" # Notarize the app diff --git a/CHANGELOG.md b/CHANGELOG.md index ae17ee6581..82e4eb0706 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1706,6 +1706,22 @@ Version 0.30.0 will be the first version with arm64 support. You can download bi This CHANGELOG.md was generated with [**Changelog for Dart**](https://pub.dartlang.org/packages/changelog) +## [2.3.3+2330] - 2023-10-18 + +### Added + +- enable emoji in web +- styling for Chat List +- new selection in chat list +- copy/paste in Chat +- update avatar in responsive screen +- Shift+Enter + +### Fixed + +- keyboard handling when scrolling chat +- click on notification redirect to exact message + ## [2.3.2+2330] - 2023-10-06 ### Added @@ -1722,9 +1738,7 @@ Dart**](https://pub.dartlang.org/packages/changelog) - gesture to back ## [2.3.1+2330] - 2023-09-29 - ### Fixed - - loading in search inside app - chat list selection - video player @@ -1770,8 +1784,9 @@ Dart**](https://pub.dartlang.org/packages/changelog) - Reduce unnecessary API request to Profile API - Fix the placeholder for image to reduce memory consumption -[2.3.2+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.3.2 +[2.3.3+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.3.3 +[2.3.2+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.3.2 [2.3.1+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.3.1 [2.3.0+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.3.0 [2.2.4+2330]: https://github.com/linagora/twake-on-matrix/releases/tag/2.2.4 diff --git a/Dockerfile b/Dockerfile index 01d3052ec6..bdda6a545f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,16 +3,17 @@ ARG FLUTTER_VERSION=3.10.6 ARG OLM_VERSION=3.2.15 # Building libolm -FROM nixos/nix AS olm-builder +# libolm only has amd64 +FROM --platform=linux/amd64 nixos/nix AS olm-builder ARG OLM_VERSION RUN nix build -v --extra-experimental-features flakes --extra-experimental-features nix-command gitlab:matrix-org/olm/${OLM_VERSION}?host=gitlab.matrix.org\#javascript -# Building Twake web files -FROM ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION} as web-builder +# Building Twake for the web +FROM --platform=linux/amd64 ghcr.io/cirruslabs/flutter:${FLUTTER_VERSION} AS web-builder COPY . /app WORKDIR /app -RUN apt update && \ - apt install openssh-client -y && \ +RUN DEBIAN_FRONTEND=noninteractive apt update && \ + apt install -y openssh-client && \ rm -rf assets/js/* && \ mkdir ~/.ssh && \ ssh-keyscan github.com >> ~/.ssh/known_hosts @@ -20,7 +21,7 @@ COPY --from=olm-builder /result/javascript assets/js/package RUN --mount=type=ssh,required=true ./scripts/build-web.sh # Final image -FROM nginx:alpine +FROM nginx:alpine AS final-image RUN rm -rf /usr/share/nginx/html COPY --from=web-builder /app/build/web /usr/share/nginx/html/web/ diff --git a/android/app/build.gradle b/android/app/build.gradle index 23419542c7..a985cab9c1 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -33,6 +33,7 @@ if (keystorePropertiesFile.exists()) { android { compileSdkVersion 33 + ndkVersion flutter.ndkVersion sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -44,7 +45,7 @@ android { defaultConfig { applicationId "com.twake.twake" - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 78d5c7eb27..700a8dbbcd 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -40,7 +40,7 @@ android:requestLegacyExternalStorage="true" android:allowBackup="false" android:fullBackupContent="false" - > + > + + diff --git a/appimage/AppImageBuilder.yml b/appimage/AppImageBuilder.yml new file mode 100644 index 0000000000..428219d48d --- /dev/null +++ b/appimage/AppImageBuilder.yml @@ -0,0 +1,65 @@ +# appimage-builder recipe see https://appimage-builder.readthedocs.io for details +script: + - rm -rf AppDir | true + - cp -r ./build/linux/x64/{{BUILD_TYPE}}/bundle AppDir + - mkdir -p AppDir/usr/share/icons/hicolor/64x64/apps/ + - cp ./assets/logo.svg AppDir/usr/share/icons/hicolor/64x64/apps/twake.svg +version: 1 +AppDir: + path: ./AppDir + app_info: + id: com.linagora.linux.twake + name: Twake + icon: twake + version: latest + exec: fluffychat + exec_args: $@ + apt: + arch: + - amd64 + allow_unauthenticated: true + sources: + - sourceline: deb http://azure.archive.ubuntu.com/ubuntu/ {{VERSION_CODENAME}} main restricted universe multiverse + key_url: "http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920D1991BC93C" + - sourceline: deb http://azure.archive.ubuntu.com/ubuntu/ {{VERSION_CODENAME}}-updates main restricted universe multiverse + key_url: "http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920D1991BC93C" + - sourceline: deb http://azure.archive.ubuntu.com/ubuntu/ {{VERSION_CODENAME}}-backports main restricted universe multiverse + key_url: "http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920D1991BC93C" + - sourceline: deb http://azure.archive.ubuntu.com/ubuntu {{VERSION_CODENAME}}-security main restricted universe multiverse + key_url: "http://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920D1991BC93C" + include: + - libgtk-3-0 + - libwayland-cursor0 + - libc6 + - libjsoncpp25 + - libsecret-1-0 + - libmpv1 + - libdrm2 + - libolm3 + files: + include: [] + exclude: + - usr/share/man + - usr/share/doc/*/README.* + - usr/share/doc/*/changelog.* + - usr/share/doc/*/NEWS.* + - usr/share/doc/*/TODO.* + test: + fedora-30: + image: appimagecrafters/tests-env:fedora-30 + command: ./AppRun + debian-stable: + image: appimagecrafters/tests-env:debian-stable + command: ./AppRun + archlinux-latest: + image: appimagecrafters/tests-env:archlinux-latest + command: ./AppRun + centos-7: + image: appimagecrafters/tests-env:centos-7 + command: ./AppRun + ubuntu-bionic: + image: appimagecrafters/tests-env:ubuntu-bionic + command: ./AppRun +AppImage: + arch: x86_64 + update-information: guess diff --git a/assets/l10n/intl_ar.arb b/assets/l10n/intl_ar.arb index 2033b5ff81..2498342cd9 100644 --- a/assets/l10n/intl_ar.arb +++ b/assets/l10n/intl_ar.arb @@ -417,11 +417,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "نزِّل الملف", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "editDisplayname": "حرر الاسم العلني", "@editDisplayname": { "type": "text", @@ -776,11 +771,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "رسالة جديدة في فلافي-شات", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "طلب تحقق جديد!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_ca.arb b/assets/l10n/intl_ca.arb index f0ddf09e39..c4d35ddfc4 100644 --- a/assets/l10n/intl_ca.arb +++ b/assets/l10n/intl_ca.arb @@ -490,11 +490,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Baixa el fitxer", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Edita", "@edit": { "type": "text", @@ -921,11 +916,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Missatge nou al FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Nova sol·licitud de verificació!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_cs.arb b/assets/l10n/intl_cs.arb index f1fcc738cf..6adfce8b6a 100644 --- a/assets/l10n/intl_cs.arb +++ b/assets/l10n/intl_cs.arb @@ -644,11 +644,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Stáhnout soubor", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Upravit", "@edit": { "type": "text", @@ -1159,11 +1154,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Nová zpráva ve FluffyChatu", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Nová žádost o ověření!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index a8606ad10f..88f57c2e35 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -639,11 +639,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Datei herunterladen", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Bearbeiten", "@edit": { "type": "text", @@ -1154,11 +1149,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Neue Nachricht in FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Neue Verifikationsanfrage!", "@newVerificationRequest": { "type": "text", @@ -2539,5 +2529,12 @@ "enterInviteLinkOrMatrixId": "Einladungslink oder Matrix-ID eingeben …", "@enterInviteLinkOrMatrixId": {}, "reopenChat": "Chat wieder eröffnen", - "@reopenChat": {} + "@reopenChat": {}, + "enterGroupName": "Chatname eingeben", + "@enterGroupName": { + "type": "text", + "placeholders": {} + }, + "invite": "Einladen", + "@invite": {} } diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 424e327543..b9d2cfea19 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -733,8 +733,8 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Download file", - "@downloadFile": { + "download": "Download", + "@download": { "type": "text", "placeholders": {} }, @@ -1262,8 +1262,8 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 New message in FluffyChat", - "@newMessageInFluffyChat": { + "newMessageInTwake": "You have 1 encrypted message", + "@newMessageInTwake": { "type": "text", "placeholders": {} }, @@ -2532,36 +2532,16 @@ "noOtherDevicesFound": "No other devices found", "fileIsTooBigForServer": "The server reports that the file is too large to be sent.", "onlineStatus": "online", - "onlineLongTimeAgo": "online long time ago", - "onlineMinAgo": "online {min} min ago", + "onlineMinAgo": "online {min}m ago", "@onlineMinAgo": { "placeholders": { "min": {} } }, - "onlineHourAgo": "online {hour} h {min} min ago", + "onlineHourAgo": "online {hour}h ago", "@onlineHourAgo": { "placeholders": { - "hour": {}, - "min": {} - } - }, - "onlineDayAgo": "online {day} day ago", - "@onlineDayAgo": { - "placeholders": { - "day": {} - } - }, - "onlineWeekAgo": "online {week} week ago", - "@onlineWeekAgo": { - "placeholders": { - "week": {} - } - }, - "onlineMonthAgo": "online {month} month ago", - "@onlineMonthAgo": { - "placeholders": { - "month": {} + "hour": {} } }, "noMessageHereYet": "No message here yet...", @@ -2755,8 +2735,16 @@ "editWorkIdentitiesDescriptions": "Edit your work identity settings such as Matrix ID, email or company name.", "copiedMatrixIdToClipboard": "Copied Matrix ID to clipboard.", "changeProfilePhoto": "Change profile photo", + "countPinChat": "PINNED CHATS ({countPinChat})", + "countAllChat": "ALL CHATS ({countAllChat})", "thisMessageHasBeenEncrypted": "This message has been encrypted", "roomCreationFailed": "Room creation failed", "errorGettingPdf": "Error getting PDF", - "errorPreviewingFile": "Error previewing file" + "errorPreviewingFile": "Error previewing file", + "paste": "Paste", + "cut": "Cut", + "pasteImageFailed": "Paste image failed", + "copyImageFailed": "Copy image failed", + "fileFormatNotSupported": "File format not supported", + "copyImageSuccess": "Image copied to clipboard" } \ No newline at end of file diff --git a/assets/l10n/intl_eo.arb b/assets/l10n/intl_eo.arb index 2a9e0f495b..4e4c904412 100644 --- a/assets/l10n/intl_eo.arb +++ b/assets/l10n/intl_eo.arb @@ -633,11 +633,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Elŝuti dosieron", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Redakti", "@edit": { "type": "text", @@ -1138,11 +1133,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Nova mesaĝo en FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Nova kontrolpeto!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index 3f9acb0e8e..d4fc035bc6 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -480,11 +480,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Descargar archivo", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "editDisplayname": "Editar nombre visible", "@editDisplayname": { "type": "text", @@ -879,11 +874,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Nuevo mensaje en FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "¡Nueva solicitud de verificación!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_et.arb b/assets/l10n/intl_et.arb index 223b4d8bf3..a17b40ae9a 100644 --- a/assets/l10n/intl_et.arb +++ b/assets/l10n/intl_et.arb @@ -644,11 +644,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Laadi fail alla", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Muuda", "@edit": { "type": "text", @@ -1159,11 +1154,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Uus sõnum FluffyChat'i vahendusel", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Uus verifitseerimispäring!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_eu.arb b/assets/l10n/intl_eu.arb index a2fe8eceff..f6762af4a6 100644 --- a/assets/l10n/intl_eu.arb +++ b/assets/l10n/intl_eu.arb @@ -407,11 +407,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Deskargatu fitxategia", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "editDisplayname": "Editatu ezizena", "@editDisplayname": { "type": "text", @@ -751,11 +746,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Mezu berria FluffyChaten", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Egiaztaketa eskaera berria!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_fa.arb b/assets/l10n/intl_fa.arb index 9e5e5161d0..60c7d6df70 100644 --- a/assets/l10n/intl_fa.arb +++ b/assets/l10n/intl_fa.arb @@ -780,11 +780,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "پرونده را بارگیرید", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "ویرایش", "@edit": { "type": "text", diff --git a/assets/l10n/intl_fi.arb b/assets/l10n/intl_fi.arb index 52488a1269..c00fc1c1a5 100644 --- a/assets/l10n/intl_fi.arb +++ b/assets/l10n/intl_fi.arb @@ -507,11 +507,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Lataa tiedosto", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Muokkaa", "@edit": { "type": "text", @@ -923,11 +918,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Uusi viesti FluffyChätissä", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Uusi varmennuspyyntö!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_fr.arb b/assets/l10n/intl_fr.arb index 53cfed1391..0372a0155f 100644 --- a/assets/l10n/intl_fr.arb +++ b/assets/l10n/intl_fr.arb @@ -648,8 +648,8 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Télécharger le fichier", - "@downloadFile": { + "download": "Télécharger", + "@download": { "type": "text", "placeholders": {} }, @@ -1163,8 +1163,8 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Nouveau message dans FluffyChat", - "@newMessageInFluffyChat": { + "newMessageInTwake": "Vous avez un message chiffré", + "@newMessageInTwake": { "type": "text", "placeholders": {} }, diff --git a/assets/l10n/intl_ga.arb b/assets/l10n/intl_ga.arb index d60456ae96..cae5d9ce43 100644 --- a/assets/l10n/intl_ga.arb +++ b/assets/l10n/intl_ga.arb @@ -1260,11 +1260,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Íoslódáil comhad", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "changePassword": "Athraigh an pasfhocal", "@changePassword": { "type": "text", @@ -1357,11 +1352,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Teachtaireacht nua i FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "needPantalaimonWarning": "Bí ar an eolas go dteastaíonn Pantalaimon uait chun criptiú ó cheann go ceann a úsáid anois.", "@needPantalaimonWarning": { "type": "text", diff --git a/assets/l10n/intl_gl.arb b/assets/l10n/intl_gl.arb index 77b2487697..27ff17451d 100644 --- a/assets/l10n/intl_gl.arb +++ b/assets/l10n/intl_gl.arb @@ -644,11 +644,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Descargar ficheiro", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Editar", "@edit": { "type": "text", @@ -1159,11 +1154,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Nova mensaxe en FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Nova solicitude de verificación!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_he.arb b/assets/l10n/intl_he.arb index c69bbf432e..8df139979d 100644 --- a/assets/l10n/intl_he.arb +++ b/assets/l10n/intl_he.arb @@ -548,11 +548,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "הורד קובץ", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "ערוך", "@edit": { "type": "text", @@ -1307,11 +1302,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "הודעה חדשה ב-FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "monday": "יום שני", "@monday": { "type": "text", diff --git a/assets/l10n/intl_hr.arb b/assets/l10n/intl_hr.arb index f60d634449..a3f4428766 100644 --- a/assets/l10n/intl_hr.arb +++ b/assets/l10n/intl_hr.arb @@ -634,11 +634,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Preuzmi datoteku", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Uredi", "@edit": { "type": "text", @@ -1132,11 +1127,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Nova poruka u FluffyChatu", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Novi zahtjev za potvrđivanje!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_hu.arb b/assets/l10n/intl_hu.arb index 814bb9ed10..b49f56e83d 100644 --- a/assets/l10n/intl_hu.arb +++ b/assets/l10n/intl_hu.arb @@ -427,11 +427,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Fájl letöltése", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "editDisplayname": "Megjelenítési név módosítása", "@editDisplayname": { "type": "text", @@ -811,11 +806,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Új FluffyChat-üzenet", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Új hitelesítési kérelem!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_id.arb b/assets/l10n/intl_id.arb index dd263d5047..2314882682 100644 --- a/assets/l10n/intl_id.arb +++ b/assets/l10n/intl_id.arb @@ -364,11 +364,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Unduh berkas", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "displaynameHasBeenChanged": "Nama tampilan telah diubah", "@displaynameHasBeenChanged": { "type": "text", @@ -947,11 +942,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Pesan baru di FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newChat": "Chat baru", "@newChat": { "type": "text", diff --git a/assets/l10n/intl_ie.arb b/assets/l10n/intl_ie.arb index d464aa5b28..9974cb8a2d 100644 --- a/assets/l10n/intl_ie.arb +++ b/assets/l10n/intl_ie.arb @@ -725,11 +725,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Descargar li file", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "emoteSettings": "Parametres de emotiones", "@emoteSettings": { "type": "text", diff --git a/assets/l10n/intl_it.arb b/assets/l10n/intl_it.arb index c2293a5bfe..b4455e79b2 100644 --- a/assets/l10n/intl_it.arb +++ b/assets/l10n/intl_it.arb @@ -532,11 +532,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Scarica il file", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Modifica", "@edit": { "type": "text", @@ -1016,11 +1011,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Nuovo messaggio in FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Nuova richiesta di verifica!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_ja.arb b/assets/l10n/intl_ja.arb index 0b3b0cf9dd..aec05e2c21 100644 --- a/assets/l10n/intl_ja.arb +++ b/assets/l10n/intl_ja.arb @@ -535,11 +535,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "ファイルのダウンロード", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "編集", "@edit": { "type": "text", @@ -1024,11 +1019,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 FluffyChatに新しいメッセージがあります", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "認証リクエスト!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_ko.arb b/assets/l10n/intl_ko.arb index 6bbd878d93..26172ace2f 100644 --- a/assets/l10n/intl_ko.arb +++ b/assets/l10n/intl_ko.arb @@ -290,11 +290,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "파일 다운로드", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "displaynameHasBeenChanged": "표시 이름이 변경되었습니다", "@displaynameHasBeenChanged": { "type": "text", @@ -1271,11 +1266,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "FluffyChat에서 새로운 메시지", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newChat": "새로운 채팅", "@newChat": { "type": "text", diff --git a/assets/l10n/intl_lt.arb b/assets/l10n/intl_lt.arb index 0eef272fac..798e45537c 100644 --- a/assets/l10n/intl_lt.arb +++ b/assets/l10n/intl_lt.arb @@ -21,11 +21,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Atsisiųsti failą", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "friday": "Penktadienis", "@friday": { "type": "text", @@ -990,11 +985,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Nauja žinutė FluffyChat'e", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Nauja patvirtinimo užklausa!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_nb.arb b/assets/l10n/intl_nb.arb index 90ac0eeb63..d4294d66ce 100644 --- a/assets/l10n/intl_nb.arb +++ b/assets/l10n/intl_nb.arb @@ -501,11 +501,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Last ned fil", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Rediger", "@edit": { "type": "text", @@ -980,11 +975,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Ny melding i FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Ny bekreftelsesforespørsel!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_nl.arb b/assets/l10n/intl_nl.arb index 06ae02e8b5..b5ed2e3e5e 100644 --- a/assets/l10n/intl_nl.arb +++ b/assets/l10n/intl_nl.arb @@ -647,11 +647,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Bestand downloaden", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Wijzig", "@edit": { "type": "text", @@ -1162,11 +1157,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Nieuw bericht in FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Nieuw verificatieverzoek!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_pl.arb b/assets/l10n/intl_pl.arb index 02ed99530b..bf0b124243 100644 --- a/assets/l10n/intl_pl.arb +++ b/assets/l10n/intl_pl.arb @@ -582,11 +582,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Pobierz plik", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Edytuj", "@edit": { "type": "text", @@ -1016,11 +1011,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Nowa wiadomość w FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Nowa prośba o weryfikację!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_pt_BR.arb b/assets/l10n/intl_pt_BR.arb index 2231709a40..9e5e948f34 100644 --- a/assets/l10n/intl_pt_BR.arb +++ b/assets/l10n/intl_pt_BR.arb @@ -643,11 +643,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Baixar arquivo", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Editar", "@edit": { "type": "text", @@ -1158,11 +1153,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Nova mensagem no FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Nova solicitação de verificação!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_pt_PT.arb b/assets/l10n/intl_pt_PT.arb index cf8d807aaf..64ae5713d9 100644 --- a/assets/l10n/intl_pt_PT.arb +++ b/assets/l10n/intl_pt_PT.arb @@ -615,11 +615,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Descarregar ficheiro", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Editar", "@edit": { "type": "text", @@ -1117,11 +1112,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Nova mensagem no FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Novo pedido de verificação!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_ru.arb b/assets/l10n/intl_ru.arb index 6891e1c511..b99d9cfa01 100644 --- a/assets/l10n/intl_ru.arb +++ b/assets/l10n/intl_ru.arb @@ -644,11 +644,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Скачать файл", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Редактировать", "@edit": { "type": "text", @@ -1159,11 +1154,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Новое сообщение во FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Новый запрос на подтверждение!", "@newVerificationRequest": { "type": "text", @@ -2553,29 +2543,20 @@ "count": {} } }, - "onlineHourAgo": "был(а) в сети {hour} ч. {min} мин. назад", + "onlineHourAgo": "был(а) в сети {hour} ч", "@onlineHourAgo": { "placeholders": { - "hour": {}, - "min": {} + "hour": {} } }, "onlineStatus": "онлайн", "@onlineStatus": {}, - "onlineLongTimeAgo": "был(а) в сети давно", - "@onlineLongTimeAgo": {}, "onlineMinAgo": "был(а) в сети {min} мин. назад", "@onlineMinAgo": { "placeholders": { "min": {} } }, - "onlineDayAgo": "был(а) в сети {day} дня назад", - "@onlineDayAgo": { - "placeholders": { - "day": {} - } - }, "chatCanHave": "В этом чате:", "@chatCanHave": {}, "channels": "Каналы", @@ -2613,18 +2594,6 @@ "type": "text", "placeholders": {} }, - "onlineWeekAgo": "был(а) в сети {week} недели назад", - "@onlineWeekAgo": { - "placeholders": { - "week": {} - } - }, - "onlineMonthAgo": "был(а) в сети {month} месяца назад", - "@onlineMonthAgo": { - "placeholders": { - "month": {} - } - }, "noMessageHereYet": "Здесь еще нет сообщений...", "@noMessageHereYet": {}, "sendMessageGuide": "Отправьте сообщение или нажмите на приветствие.", @@ -2790,5 +2759,146 @@ "searchSuggestion": "Вы можете искать пользователей по имени или публичному адресу сервера", "@searchSuggestion": {}, "forwardTo": "Переслать...", - "@forwardTo": {} + "@forwardTo": {}, + "noMoreResult": "Больше нет результатов", + "@noMoreResult": {}, + "errorPageTitle": "Что-то пошло не так", + "@errorPageTitle": {}, + "chatFolders": "Папки", + "@chatFolders": {}, + "errorPageDescription": "Этой страницы не существует.", + "@errorPageDescription": {}, + "downloadImageError": "Не удалось сохранить", + "@downloadImageError": {}, + "appLanguage": "Язык приложения", + "@appLanguage": {}, + "copyMessageText": "Скопировать", + "@copyMessageText": {}, + "membersInfo": "Участники ({count})", + "@membersInfo": { + "placeholders": { + "count": {} + } + }, + "recentChat": "ПОСЛЕДНИЕ ЧАТЫ", + "@recentChat": {}, + "muteThisMessage": "Отключить уведомления", + "@muteThisMessage": {}, + "editProfileDescriptions": "Обновляйте профиль – имя, изображение и краткое описание.", + "@editProfileDescriptions": {}, + "cannotEnableKeyBackup": "Невозможно включить резервное копирование. Попробуйте еще раз в настройках.", + "@cannotEnableKeyBackup": { + "type": "text", + "placeholders": {} + }, + "settingsHelpSubtitle": "Помощь, обратная связь, политика конфиденциальности.", + "@settingsHelpSubtitle": {}, + "errorPreviewingFile": "Предварительный просмотр недоступен", + "@errorPreviewingFile": {}, + "unread": "Непрочитаное", + "@unread": {}, + "company": "Компания", + "@company": {}, + "addMembers": "Добавить участников", + "@addMembers": {}, + "email": "Email", + "@email": {}, + "bio": "Био (опционально)", + "@bio": {}, + "searchForPeopleAndChannels": "Искать людей и каналы", + "@searchForPeopleAndChannels": {}, + "markThisMessageAsRead": "Отметить как прочитанное", + "@markThisMessageAsRead": {}, + "downloads": "Загрузки", + "@downloads": {}, + "changeProfilePhoto": "Изменить фото профиля", + "@changeProfilePhoto": {}, + "errorPageButton": "Назад к чату", + "@errorPageButton": {}, + "playVideo": "Проиграть", + "@playVideo": {}, + "settingsPrivacyAndSecuritySubtitle": "Блокировка контактов, исчезающие сообщения.", + "@settingsPrivacyAndSecuritySubtitle": {}, + "copiedMatrixIdToClipboard": "Matrix ID скопирован в буфер обмена.", + "@copiedMatrixIdToClipboard": {}, + "markThisMessageAsUnRead": "Отметить как непрочитанное", + "@markThisMessageAsUnRead": {}, + "privacyAndSecurity": "Конфиденциальность и безопасность", + "@privacyAndSecurity": {}, + "matrixId": "Matrix ID", + "@matrixId": {}, + "roomCreationFailed": "Не удалось создать комнату", + "@roomCreationFailed": {}, + "settingsAppLanguageSubtitle": "Английский (язык телефона).", + "@settingsAppLanguageSubtitle": {}, + "settingsNotificationAndSoundsSubtitle": "Настройте уведомления от Twake – предварительный просмотр сообщений, звук, время и т. д.", + "@settingsNotificationAndSoundsSubtitle": {}, + "mute": "Отключить уведомления", + "@mute": {}, + "unpinThisMessage": "Открепить", + "@unpinThisMessage": {}, + "notInAChatYet": "У вас еще нет чатов", + "@notInAChatYet": {}, + "basicInfo": "ИНФОРМАЦИЯ ПРОФИЛЯ", + "@basicInfo": {}, + "downloadFileInWeb": "Файл сохранен в {directory}", + "@downloadFileInWeb": { + "placeholders": { + "directory": {} + } + }, + "previous": "Назад", + "@previous": {}, + "files": "Файлы", + "@files": {}, + "read": "Читать", + "@read": {}, + "displayName": "Отображаемое имя", + "@displayName": {}, + "select": "Выбрать", + "@select": {}, + "blankChatTitle": "Выберите чат или нажмите на #EditIcon#, чтобы создать его.", + "@blankChatTitle": {}, + "editWorkIdentitiesDescriptions": "Изменяйте свои идентификаторы, такие как Matrix ID, адрес электронной почты или название компании.", + "@editWorkIdentitiesDescriptions": {}, + "links": "Ссылки", + "@links": {}, + "downloadImageSuccess": "Изображение сохранено", + "@downloadImageSuccess": {}, + "chatInfo": "Информация", + "@chatInfo": {}, + "settingsDevicesSubtitle": "Контролируйте вход и выход на любом устройстве.", + "@settingsDevicesSubtitle": {}, + "unmuteThisMessage": "Включить уведомления", + "@unmuteThisMessage": {}, + "pinThisMessage": "Закрепить", + "@pinThisMessage": {}, + "thisMessageHasBeenEncrypted": "Это сообщение зашифровано", + "@thisMessageHasBeenEncrypted": {}, + "media": "Медиа", + "@media": {}, + "cannotUnlockBackupKey": "Невозможно разблокировать резервную копию ключа.", + "@cannotUnlockBackupKey": {}, + "workIdentitiesInfo": "ИДЕНТИФИКАТОРЫ", + "@workIdentitiesInfo": {}, + "members": "Участники", + "@members": {}, + "errorGettingPdf": "Не удалось загрузить PDF", + "@errorGettingPdf": {}, + "unmute": "Включить уведомления", + "@unmute": {}, + "selectChat": "Выбрать чат", + "@selectChat": {}, + "add": "Добавить", + "@add": {}, + "cannotUploadKey": "Невозможно сохранить резервную копию ключа.", + "@cannotUploadKey": {}, + "notificationAndSounds": "Уведомления и звук", + "@notificationAndSounds": {}, + "settingsChatSubtitle": "Внешний вид, темы, обои, история чата.", + "@settingsChatSubtitle": {}, + "settingsChatFoldersSubtitle": "Создавайте папки для разных групп чатов и быстро переключайтесь между ними.", + "@settingsChatFoldersSubtitle": {}, + "done": "Готово", + "@done": {} } diff --git a/assets/l10n/intl_sk.arb b/assets/l10n/intl_sk.arb index c345c3e250..486cdb8083 100644 --- a/assets/l10n/intl_sk.arb +++ b/assets/l10n/intl_sk.arb @@ -400,11 +400,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Stiahnuť súbor", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "editDisplayname": "Zmeniť prezývku", "@editDisplayname": { "type": "text", @@ -732,11 +727,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Nová správa v FluffyChate", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Nová žiadosť o verifikáciu!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_sr.arb b/assets/l10n/intl_sr.arb index 2a537f5e04..e4128679e9 100644 --- a/assets/l10n/intl_sr.arb +++ b/assets/l10n/intl_sr.arb @@ -604,11 +604,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Преузми фајл", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Уреди", "@edit": { "type": "text", @@ -1100,11 +1095,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "Нова порука — FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Нови захтев за верификацију!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_sv.arb b/assets/l10n/intl_sv.arb index f2fee61625..5b5ff216e5 100644 --- a/assets/l10n/intl_sv.arb +++ b/assets/l10n/intl_sv.arb @@ -494,11 +494,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Ladda ner fil", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Ändra", "@edit": { "type": "text", @@ -963,11 +958,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Nya meddelanden i FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Ny verifikationsbegäran!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_tr.arb b/assets/l10n/intl_tr.arb index d3575b2189..bc90e63c9a 100644 --- a/assets/l10n/intl_tr.arb +++ b/assets/l10n/intl_tr.arb @@ -648,11 +648,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Dosyayı indir", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "Düzenle", "@edit": { "type": "text", @@ -1163,11 +1158,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 FluffyChat'te yeni mesaj", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Yeni doğrulama isteği!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_uk.arb b/assets/l10n/intl_uk.arb index f0788ffb94..26fbba2485 100644 --- a/assets/l10n/intl_uk.arb +++ b/assets/l10n/intl_uk.arb @@ -407,11 +407,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Завантажити файл", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "editDisplayname": "Змінити показуване ім'я", "@editDisplayname": { "type": "text", @@ -746,11 +741,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Нове повідомлення у FluffyChat", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "Новий запит перевірки!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_vi.arb b/assets/l10n/intl_vi.arb index fbd7e8c970..d0506644cb 100644 --- a/assets/l10n/intl_vi.arb +++ b/assets/l10n/intl_vi.arb @@ -126,8 +126,8 @@ "type": "text", "placeholders": {} }, - "downloadFile": "Tải ảnh xuống", - "@downloadFile": { + "download": "Tải xuống", + "@download": { "type": "text", "placeholders": {} }, @@ -1504,8 +1504,8 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 Tin nhắn mới trong FluffyChat", - "@newMessageInFluffyChat": { + "newMessageInTwake": "Bạn có tin nhắn mới", + "@newMessageInTwake": { "type": "text", "placeholders": {} }, @@ -2351,11 +2351,10 @@ }, "newGroupChat": "Nhóm trò chuyện mới", "@newGroupChat": {}, - "onlineHourAgo": "trực truyến {hour} giờ {min} phút trước", + "onlineHourAgo": "trực truyến {hour} giờ trước", "@onlineHourAgo": { "placeholders": { - "hour": {}, - "min": {} + "hour": {} } }, "addACaption": "Thêm chú thích...", @@ -2368,12 +2367,6 @@ "username2": {} } }, - "onlineWeekAgo": "trực tuyến {week} tuần trước", - "@onlineWeekAgo": { - "placeholders": { - "week": {} - } - }, "tuesday": "Thứ ba", "@tuesday": { "type": "text", @@ -2397,8 +2390,6 @@ "type": "text", "placeholders": {} }, - "onlineLongTimeAgo": "trực tuyến một khoảng thời gian trước", - "@onlineLongTimeAgo": {}, "reopenChat": "Mở lại cuộc trò chuyện", "@reopenChat": {}, "inactive": "Chưa kích hoạt", @@ -2570,12 +2561,6 @@ "targetName": {} } }, - "onlineDayAgo": "trực tuyến {day} ngày trước", - "@onlineDayAgo": { - "placeholders": { - "day": {} - } - }, "tooManyRequestsWarning": "Quá nhiều yêu cầu. Vui lòng thử lại sau!", "@tooManyRequestsWarning": { "type": "text", @@ -2658,12 +2643,6 @@ "@wrongServerName": {}, "twakeUsers": "Người dùng Twake", "@twakeUsers": {}, - "onlineMonthAgo": "trực tuyến {month} tháng trước", - "@onlineMonthAgo": { - "placeholders": { - "month": {} - } - }, "newGroup": "Cuộc trò chuyện mới", "@newGroup": {}, "enterInviteLinkOrMatrixId": "Nhập liên kết mời hoặc Matrix ID...", diff --git a/assets/l10n/intl_zh.arb b/assets/l10n/intl_zh.arb index 1090ad70ad..e3093c673d 100644 --- a/assets/l10n/intl_zh.arb +++ b/assets/l10n/intl_zh.arb @@ -622,11 +622,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "下载文件", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "编辑", "@edit": { "type": "text", @@ -1118,11 +1113,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "💬 FluffyChat 新消息", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "新的验证请求!", "@newVerificationRequest": { "type": "text", diff --git a/assets/l10n/intl_zh_Hant.arb b/assets/l10n/intl_zh_Hant.arb index 72208a70cf..de0a750add 100644 --- a/assets/l10n/intl_zh_Hant.arb +++ b/assets/l10n/intl_zh_Hant.arb @@ -527,11 +527,6 @@ "type": "text", "placeholders": {} }, - "downloadFile": "下載文件", - "@downloadFile": { - "type": "text", - "placeholders": {} - }, "edit": "編輯", "@edit": { "type": "text", @@ -1006,11 +1001,6 @@ "type": "text", "placeholders": {} }, - "newMessageInFluffyChat": "來自 FluffyChat 的新訊息", - "@newMessageInFluffyChat": { - "type": "text", - "placeholders": {} - }, "newVerificationRequest": "新的驗證請求!", "@newVerificationRequest": { "type": "text", diff --git a/fonts/NotoEmoji/NotoColorEmoji.ttf b/fonts/NotoEmoji/NotoColorEmoji.ttf deleted file mode 100644 index 8ebcc8fde2..0000000000 Binary files a/fonts/NotoEmoji/NotoColorEmoji.ttf and /dev/null differ diff --git a/fonts/NotoEmoji/NotoEmoji-Regular.ttf b/fonts/NotoEmoji/NotoEmoji-Regular.ttf deleted file mode 100644 index f3b247d43d..0000000000 Binary files a/fonts/NotoEmoji/NotoEmoji-Regular.ttf and /dev/null differ diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart index 4e7b8be862..eec6f1b5a1 100644 --- a/integration_test/app_test.dart +++ b/integration_test/app_test.dart @@ -1,6 +1,6 @@ import 'package:fluffychat/config/setting_keys.dart'; import 'package:fluffychat/pages/chat/chat_view.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_body.dart'; +import 'package:fluffychat/pages/chat_list/chat_list_body_view.dart'; import 'package:fluffychat/pages/chat_list/search_title.dart'; import 'package:fluffychat/pages/new_group/contacts_selection_view.dart'; @@ -66,7 +66,7 @@ void main() { 500, scrollable: find .descendant( - of: find.byType(ChatListViewBody), + of: find.byType(ChatListBodyView), matching: find.byType(Scrollable), ) .first, @@ -82,7 +82,7 @@ void main() { 500, scrollable: find .descendant( - of: find.byType(ChatListViewBody), + of: find.byType(ChatListBodyView), matching: find.byType(Scrollable), ) .first, diff --git a/integration_test/extensions/default_flows.dart b/integration_test/extensions/default_flows.dart index 6609969004..eb642625a4 100644 --- a/integration_test/extensions/default_flows.dart +++ b/integration_test/extensions/default_flows.dart @@ -1,6 +1,6 @@ import 'dart:developer'; -import 'package:fluffychat/pages/chat_list/chat_list_body.dart'; +import 'package:fluffychat/pages/chat_list/chat_list_body_view.dart'; import 'package:fluffychat/pages/homeserver_picker/homeserver_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -82,7 +82,7 @@ extension DefaultFlowExtensions on WidgetTester { } await tester.waitFor( - find.byType(ChatListViewBody), + find.byType(ChatListBodyView), skipPumpAndSettle: true, ); } @@ -105,7 +105,7 @@ extension DefaultFlowExtensions on WidgetTester { Future ensureLoggedOut() async { final tester = this; await tester.pumpAndSettle(); - if (find.byType(ChatListViewBody).evaluate().isNotEmpty) { + if (find.byType(ChatListBodyView).evaluate().isNotEmpty) { await tester.tap(find.byTooltip('Show menu')); await tester.pumpAndSettle(); await tester.tap(find.text('Settings')); @@ -133,7 +133,7 @@ extension DefaultFlowExtensions on WidgetTester { await tester.pumpAndSettle(); final homeserverPickerFinder = find.byType(HomeserverPicker); - final chatListFinder = find.byType(ChatListViewBody); + final chatListFinder = find.byType(ChatListBodyView); final end = DateTime.now().add(timeout); diff --git a/ios/FluffyChat Share/Base.lproj/MainInterface.storyboard b/ios/FluffyChat Share/Base.lproj/MainInterface.storyboard deleted file mode 100644 index 286a50894d..0000000000 --- a/ios/FluffyChat Share/Base.lproj/MainInterface.storyboard +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Podfile b/ios/Podfile index 95b266171d..90a88d6855 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -32,6 +32,10 @@ target 'Runner' do use_modular_headers! flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + + target 'Twake Share' do + inherit! :search_paths + end end post_install do |installer| diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 38be348e90..cf83010208 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -11,14 +11,14 @@ 0611A7F22A678C7700F180CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0611A7F42A678C7700F180CC /* Localizable.strings */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 643D9A430DCE4893FC15438F /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B16AB66899BBD65C9BFB2599 /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 81048B2F1319025A0C39525B /* Pods_Twake_Share.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B8CAF7CDF8EA1DB709D295C3 /* Pods_Twake_Share.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - AB0F2865DE230DE37373E0E0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 50DEFC207B70632D9C56ED78 /* Pods_Runner.framework */; }; C1005C45261071B5002F4F32 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1005C44261071B5002F4F32 /* ShareViewController.swift */; }; - C1005C48261071B5002F4F32 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C1005C46261071B5002F4F32 /* MainInterface.storyboard */; }; - C1005C4C261071B5002F4F32 /* FluffyChat Share.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = C1005C42261071B5002F4F32 /* FluffyChat Share.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + C1005C4C261071B5002F4F32 /* Twake Share.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = C1005C42261071B5002F4F32 /* Twake Share.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; C149567C25C7274F00A16396 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = C149567B25C7274F00A16396 /* GoogleService-Info.plist */; }; /* End PBXBuildFile section */ @@ -28,7 +28,7 @@ containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; remoteGlobalIDString = C1005C41261071B5002F4F32; - remoteInfo = "FluffyChat Share"; + remoteInfo = "Twake Share"; }; /* End PBXContainerItemProxy section */ @@ -49,7 +49,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( - C1005C4C261071B5002F4F32 /* FluffyChat Share.appex in Embed App Extensions */, + C1005C4C261071B5002F4F32 /* Twake Share.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -58,13 +58,88 @@ /* Begin PBXFileReference section */ 0611A7F32A678C7700F180CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB3E12ADE390500E09F51 /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB3E22ADE39B400E09F51 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Main.strings; sourceTree = ""; }; + 06AAB3E32ADE39B400E09F51 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB3E42ADE39B400E09F51 /* ca */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ca; path = ca.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB3E62ADE39C100E09F51 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Main.strings; sourceTree = ""; }; + 06AAB3E72ADE39C100E09F51 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB3E82ADE39C200E09F51 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB3EA2ADE39CA00E09F51 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Main.strings; sourceTree = ""; }; + 06AAB3EB2ADE39CA00E09F51 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB3EC2ADE39CB00E09F51 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB3EE2ADE39EB00E09F51 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eo; path = eo.lproj/Main.strings; sourceTree = ""; }; + 06AAB3EF2ADE39EB00E09F51 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eo; path = eo.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB3F02ADE39EB00E09F51 /* eo */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eo; path = eo.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB3F22ADE39FA00E09F51 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Main.strings; sourceTree = ""; }; + 06AAB3F32ADE39FA00E09F51 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB3F42ADE39FB00E09F51 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB3F62ADE3A1E00E09F51 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Main.strings; sourceTree = ""; }; + 06AAB3F72ADE3A1E00E09F51 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB3F82ADE3A1E00E09F51 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB3FA2ADE3B0400E09F51 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Main.strings; sourceTree = ""; }; + 06AAB3FB2ADE3B0400E09F51 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB3FC2ADE3B0400E09F51 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB3FE2ADE3BE200E09F51 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Main.strings; sourceTree = ""; }; + 06AAB3FF2ADE3BE200E09F51 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB4002ADE3BE200E09F51 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB4012ADE3C1400E09F51 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/Main.strings; sourceTree = ""; }; + 06AAB4022ADE3C1400E09F51 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB4032ADE3C1400E09F51 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = gl.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB4042ADE3C3100E09F51 /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/Main.strings"; sourceTree = ""; }; + 06AAB4052ADE3C3100E09F51 /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 06AAB4062ADE3C3100E09F51 /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/Localizable.strings"; sourceTree = ""; }; + 06AAB4072ADE3C7A00E09F51 /* hr-HR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hr-HR"; path = "hr-HR.lproj/Main.strings"; sourceTree = ""; }; + 06AAB4082ADE3C7A00E09F51 /* hr-HR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hr-HR"; path = "hr-HR.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 06AAB4092ADE3C7A00E09F51 /* hr-HR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hr-HR"; path = "hr-HR.lproj/Localizable.strings"; sourceTree = ""; }; + 06AAB40A2ADE3CB600E09F51 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Main.strings; sourceTree = ""; }; + 06AAB40B2ADE3CB600E09F51 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB40C2ADE3CB600E09F51 /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB40D2ADE3CFD00E09F51 /* hy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hy; path = hy.lproj/Main.strings; sourceTree = ""; }; + 06AAB40E2ADE3CFE00E09F51 /* hy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hy; path = hy.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB40F2ADE3CFE00E09F51 /* hy */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hy; path = hy.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB4102ADE3D0D00E09F51 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Main.strings; sourceTree = ""; }; + 06AAB4112ADE3D0D00E09F51 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB4122ADE3D0D00E09F51 /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB4132ADE3D2200E09F51 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Main.strings; sourceTree = ""; }; + 06AAB4142ADE3D2200E09F51 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB4152ADE3D2200E09F51 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB4162ADE3D9D00E09F51 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Main.strings"; sourceTree = ""; }; + 06AAB4172ADE3D9D00E09F51 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 06AAB4182ADE3D9D00E09F51 /* nb-NO */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "nb-NO"; path = "nb-NO.lproj/Localizable.strings"; sourceTree = ""; }; + 06AAB4192ADE3DB700E09F51 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Main.strings; sourceTree = ""; }; + 06AAB41A2ADE3DB700E09F51 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB41B2ADE3DB700E09F51 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB41C2ADE3DCA00E09F51 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Main.strings; sourceTree = ""; }; + 06AAB41D2ADE3DCA00E09F51 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB41E2ADE3DCA00E09F51 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB41F2ADE3DD900E09F51 /* ru-RU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ru-RU"; path = "ru-RU.lproj/Main.strings"; sourceTree = ""; }; + 06AAB4202ADE3DD900E09F51 /* ru-RU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ru-RU"; path = "ru-RU.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 06AAB4212ADE3DDA00E09F51 /* ru-RU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ru-RU"; path = "ru-RU.lproj/Localizable.strings"; sourceTree = ""; }; + 06AAB4222ADE3E3000E09F51 /* sk-SK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sk-SK"; path = "sk-SK.lproj/Main.strings"; sourceTree = ""; }; + 06AAB4232ADE3E3000E09F51 /* sk-SK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sk-SK"; path = "sk-SK.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 06AAB4242ADE3E3000E09F51 /* sk-SK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sk-SK"; path = "sk-SK.lproj/Localizable.strings"; sourceTree = ""; }; + 06AAB4252ADE3E5100E09F51 /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/Main.strings"; sourceTree = ""; }; + 06AAB4262ADE3E5100E09F51 /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 06AAB4272ADE3E5100E09F51 /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/Localizable.strings"; sourceTree = ""; }; + 06AAB4282ADE3F5200E09F51 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Main.strings; sourceTree = ""; }; + 06AAB4292ADE3F5200E09F51 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB42A2ADE3F5200E09F51 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB42B2ADE3F6A00E09F51 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Main.strings; sourceTree = ""; }; + 06AAB42C2ADE3F6A00E09F51 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/LaunchScreen.strings; sourceTree = ""; }; + 06AAB42D2ADE3F6A00E09F51 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + 06AAB42E2ADE3F7700E09F51 /* vi-VN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "vi-VN"; path = "vi-VN.lproj/Main.strings"; sourceTree = ""; }; + 06AAB42F2ADE3F7700E09F51 /* vi-VN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "vi-VN"; path = "vi-VN.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 06AAB4302ADE3F7700E09F51 /* vi-VN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "vi-VN"; path = "vi-VN.lproj/Localizable.strings"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1B6C59111A74FF0BB750A10A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 216064C73ECC2AECB9F0A1FA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 50DEFC207B70632D9C56ED78 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 536699DF3AE0B9D3AF31677B /* Pods-Twake Share.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Twake Share.profile.xcconfig"; path = "Target Support Files/Pods-Twake Share/Pods-Twake Share.profile.xcconfig"; sourceTree = ""; }; + 539142E4CD4E28C397AF0C3B /* Pods-Twake Share.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Twake Share.debug.xcconfig"; path = "Target Support Files/Pods-Twake Share/Pods-Twake Share.debug.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 76737C9A857D5FD6D2634A3F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -73,15 +148,16 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9DB2F3524376810E74C799A8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - C1005C42261071B5002F4F32 /* FluffyChat Share.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "FluffyChat Share.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + B16AB66899BBD65C9BFB2599 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B8CAF7CDF8EA1DB709D295C3 /* Pods_Twake_Share.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Twake_Share.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BA9BBC5546D378D952097E1E /* Pods-Twake Share.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Twake Share.release.xcconfig"; path = "Target Support Files/Pods-Twake Share/Pods-Twake Share.release.xcconfig"; sourceTree = ""; }; + C1005C42261071B5002F4F32 /* Twake Share.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Twake Share.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; C1005C44261071B5002F4F32 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; - C1005C47261071B5002F4F32 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; C1005C49261071B5002F4F32 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C1005C53261072D4002F4F32 /* FluffyChat Share.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "FluffyChat Share.entitlements"; sourceTree = ""; }; + C1005C53261072D4002F4F32 /* Twake Share.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Twake Share.entitlements"; sourceTree = ""; }; C149567B25C7274F00A16396 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; C149567D25C7276200A16396 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; - EA246783222E02DD03959891 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + FDBA7311CF00074CB7786C33 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -89,7 +165,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - AB0F2865DE230DE37373E0E0 /* Pods_Runner.framework in Frameworks */, + 643D9A430DCE4893FC15438F /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -97,16 +173,18 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 81048B2F1319025A0C39525B /* Pods_Twake_Share.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 075EE1BE25359E34308E0B78 /* Frameworks */ = { + 17347C56C756794A41C124FE /* Frameworks */ = { isa = PBXGroup; children = ( - 50DEFC207B70632D9C56ED78 /* Pods_Runner.framework */, + B16AB66899BBD65C9BFB2599 /* Pods_Runner.framework */, + B8CAF7CDF8EA1DB709D295C3 /* Pods_Twake_Share.framework */, ); name = Frameworks; sourceTree = ""; @@ -118,7 +196,6 @@ 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, - 0611A7F42A678C7700F180CC /* Localizable.strings */, ); name = Flutter; sourceTree = ""; @@ -128,10 +205,10 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - C1005C43261071B5002F4F32 /* FluffyChat Share */, + C1005C43261071B5002F4F32 /* Twake Share */, 97C146EF1CF9000F007C117D /* Products */, E89DCAC000D371640E94E65B /* Pods */, - 075EE1BE25359E34308E0B78 /* Frameworks */, + 17347C56C756794A41C124FE /* Frameworks */, ); sourceTree = ""; }; @@ -139,7 +216,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - C1005C42261071B5002F4F32 /* FluffyChat Share.appex */, + C1005C42261071B5002F4F32 /* Twake Share.appex */, ); name = Products; sourceTree = ""; @@ -147,6 +224,7 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + 0611A7F42A678C7700F180CC /* Localizable.strings */, C149567D25C7276200A16396 /* Runner.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -161,23 +239,25 @@ path = Runner; sourceTree = ""; }; - C1005C43261071B5002F4F32 /* FluffyChat Share */ = { + C1005C43261071B5002F4F32 /* Twake Share */ = { isa = PBXGroup; children = ( - C1005C53261072D4002F4F32 /* FluffyChat Share.entitlements */, + C1005C53261072D4002F4F32 /* Twake Share.entitlements */, C1005C44261071B5002F4F32 /* ShareViewController.swift */, - C1005C46261071B5002F4F32 /* MainInterface.storyboard */, C1005C49261071B5002F4F32 /* Info.plist */, ); - path = "FluffyChat Share"; + path = "Twake Share"; sourceTree = ""; }; E89DCAC000D371640E94E65B /* Pods */ = { isa = PBXGroup; children = ( - 76737C9A857D5FD6D2634A3F /* Pods-Runner.debug.xcconfig */, - EA246783222E02DD03959891 /* Pods-Runner.release.xcconfig */, - 9DB2F3524376810E74C799A8 /* Pods-Runner.profile.xcconfig */, + 1B6C59111A74FF0BB750A10A /* Pods-Runner.debug.xcconfig */, + FDBA7311CF00074CB7786C33 /* Pods-Runner.release.xcconfig */, + 216064C73ECC2AECB9F0A1FA /* Pods-Runner.profile.xcconfig */, + 539142E4CD4E28C397AF0C3B /* Pods-Twake Share.debug.xcconfig */, + BA9BBC5546D378D952097E1E /* Pods-Twake Share.release.xcconfig */, + 536699DF3AE0B9D3AF31677B /* Pods-Twake Share.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -189,7 +269,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 8C9CCA7C5C45651F90C7BFDD /* [CP] Check Pods Manifest.lock */, + 976327FA3CEAB4BFF28CC739 /* [CP] Check Pods Manifest.lock */, C1005C4D261071B5002F4F32 /* Embed App Extensions */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, @@ -197,7 +277,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - F9C8EE392B9AB471149C306E /* [CP] Embed Pods Frameworks */, + 5F91C032622C7759B233FC58 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -209,10 +289,11 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; - C1005C41261071B5002F4F32 /* FluffyChat Share */ = { + C1005C41261071B5002F4F32 /* Twake Share */ = { isa = PBXNativeTarget; - buildConfigurationList = C1005C51261071B5002F4F32 /* Build configuration list for PBXNativeTarget "FluffyChat Share" */; + buildConfigurationList = C1005C51261071B5002F4F32 /* Build configuration list for PBXNativeTarget "Twake Share" */; buildPhases = ( + F147DF49C55416DBE4109E8B /* [CP] Check Pods Manifest.lock */, C1005C3E261071B5002F4F32 /* Sources */, C1005C3F261071B5002F4F32 /* Frameworks */, C1005C40261071B5002F4F32 /* Resources */, @@ -221,9 +302,9 @@ ); dependencies = ( ); - name = "FluffyChat Share"; - productName = "FluffyChat Share"; - productReference = C1005C42261071B5002F4F32 /* FluffyChat Share.appex */; + name = "Twake Share"; + productName = "Twake Share"; + productReference = C1005C42261071B5002F4F32 /* Twake Share.appex */; productType = "com.apple.product-type.app-extension"; }; /* End PBXNativeTarget section */ @@ -252,6 +333,31 @@ knownRegions = ( en, Base, + ar, + ca, + cs, + de, + eo, + es, + et, + eu, + fr, + gl, + "hu-HU", + "hr-HR", + hu, + hy, + it, + ja, + "nb-NO", + pl, + pt, + "ru-RU", + "sk-SK", + "sv-SE", + tr, + uk, + "vi-VN", ); mainGroup = 97C146E51CF9000F007C117D; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; @@ -259,7 +365,7 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - C1005C41261071B5002F4F32 /* FluffyChat Share */, + C1005C41261071B5002F4F32 /* Twake Share */, ); }; /* End PBXProject section */ @@ -282,7 +388,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - C1005C48261071B5002F4F32 /* MainInterface.storyboard in Resources */, 0611A7F22A678C7700F180CC /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -306,26 +411,21 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 8C9CCA7C5C45651F90C7BFDD /* [CP] Check Pods Manifest.lock */ = { + 5F91C032622C7759B233FC58 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { @@ -343,21 +443,48 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - F9C8EE392B9AB471149C306E /* [CP] Embed Pods Frameworks */ = { + 976327FA3CEAB4BFF28CC739 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F147DF49C55416DBE4109E8B /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Twake Share-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -385,7 +512,7 @@ /* Begin PBXTargetDependency section */ C1005C4B261071B5002F4F32 /* PBXTargetDependency */ = { isa = PBXTargetDependency; - target = C1005C41261071B5002F4F32 /* FluffyChat Share */; + target = C1005C41261071B5002F4F32 /* Twake Share */; targetProxy = C1005C4A261071B5002F4F32 /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -395,6 +522,31 @@ isa = PBXVariantGroup; children = ( 0611A7F32A678C7700F180CC /* en */, + 06AAB3E12ADE390500E09F51 /* ar */, + 06AAB3E42ADE39B400E09F51 /* ca */, + 06AAB3E82ADE39C200E09F51 /* cs */, + 06AAB3EC2ADE39CB00E09F51 /* de */, + 06AAB3F02ADE39EB00E09F51 /* eo */, + 06AAB3F42ADE39FB00E09F51 /* es */, + 06AAB3F82ADE3A1E00E09F51 /* et */, + 06AAB3FC2ADE3B0400E09F51 /* eu */, + 06AAB4002ADE3BE200E09F51 /* fr */, + 06AAB4032ADE3C1400E09F51 /* gl */, + 06AAB4062ADE3C3100E09F51 /* hu-HU */, + 06AAB4092ADE3C7A00E09F51 /* hr-HR */, + 06AAB40C2ADE3CB600E09F51 /* hu */, + 06AAB40F2ADE3CFE00E09F51 /* hy */, + 06AAB4122ADE3D0D00E09F51 /* it */, + 06AAB4152ADE3D2200E09F51 /* ja */, + 06AAB4182ADE3D9D00E09F51 /* nb-NO */, + 06AAB41B2ADE3DB700E09F51 /* pl */, + 06AAB41E2ADE3DCA00E09F51 /* pt */, + 06AAB4212ADE3DDA00E09F51 /* ru-RU */, + 06AAB4242ADE3E3000E09F51 /* sk-SK */, + 06AAB4272ADE3E5100E09F51 /* sv-SE */, + 06AAB42A2ADE3F5200E09F51 /* tr */, + 06AAB42D2ADE3F6A00E09F51 /* uk */, + 06AAB4302ADE3F7700E09F51 /* vi-VN */, ); name = Localizable.strings; sourceTree = ""; @@ -403,6 +555,30 @@ isa = PBXVariantGroup; children = ( 97C146FB1CF9000F007C117D /* Base */, + 06AAB3E22ADE39B400E09F51 /* ca */, + 06AAB3E62ADE39C100E09F51 /* cs */, + 06AAB3EA2ADE39CA00E09F51 /* de */, + 06AAB3EE2ADE39EB00E09F51 /* eo */, + 06AAB3F22ADE39FA00E09F51 /* es */, + 06AAB3F62ADE3A1E00E09F51 /* et */, + 06AAB3FA2ADE3B0400E09F51 /* eu */, + 06AAB3FE2ADE3BE200E09F51 /* fr */, + 06AAB4012ADE3C1400E09F51 /* gl */, + 06AAB4042ADE3C3100E09F51 /* hu-HU */, + 06AAB4072ADE3C7A00E09F51 /* hr-HR */, + 06AAB40A2ADE3CB600E09F51 /* hu */, + 06AAB40D2ADE3CFD00E09F51 /* hy */, + 06AAB4102ADE3D0D00E09F51 /* it */, + 06AAB4132ADE3D2200E09F51 /* ja */, + 06AAB4162ADE3D9D00E09F51 /* nb-NO */, + 06AAB4192ADE3DB700E09F51 /* pl */, + 06AAB41C2ADE3DCA00E09F51 /* pt */, + 06AAB41F2ADE3DD900E09F51 /* ru-RU */, + 06AAB4222ADE3E3000E09F51 /* sk-SK */, + 06AAB4252ADE3E5100E09F51 /* sv-SE */, + 06AAB4282ADE3F5200E09F51 /* tr */, + 06AAB42B2ADE3F6A00E09F51 /* uk */, + 06AAB42E2ADE3F7700E09F51 /* vi-VN */, ); name = Main.storyboard; sourceTree = ""; @@ -411,18 +587,34 @@ isa = PBXVariantGroup; children = ( 97C147001CF9000F007C117D /* Base */, + 06AAB3E32ADE39B400E09F51 /* ca */, + 06AAB3E72ADE39C100E09F51 /* cs */, + 06AAB3EB2ADE39CA00E09F51 /* de */, + 06AAB3EF2ADE39EB00E09F51 /* eo */, + 06AAB3F32ADE39FA00E09F51 /* es */, + 06AAB3F72ADE3A1E00E09F51 /* et */, + 06AAB3FB2ADE3B0400E09F51 /* eu */, + 06AAB3FF2ADE3BE200E09F51 /* fr */, + 06AAB4022ADE3C1400E09F51 /* gl */, + 06AAB4052ADE3C3100E09F51 /* hu-HU */, + 06AAB4082ADE3C7A00E09F51 /* hr-HR */, + 06AAB40B2ADE3CB600E09F51 /* hu */, + 06AAB40E2ADE3CFE00E09F51 /* hy */, + 06AAB4112ADE3D0D00E09F51 /* it */, + 06AAB4142ADE3D2200E09F51 /* ja */, + 06AAB4172ADE3D9D00E09F51 /* nb-NO */, + 06AAB41A2ADE3DB700E09F51 /* pl */, + 06AAB41D2ADE3DCA00E09F51 /* pt */, + 06AAB4202ADE3DD900E09F51 /* ru-RU */, + 06AAB4232ADE3E3000E09F51 /* sk-SK */, + 06AAB4262ADE3E5100E09F51 /* sv-SE */, + 06AAB4292ADE3F5200E09F51 /* tr */, + 06AAB42C2ADE3F6A00E09F51 /* uk */, + 06AAB42F2ADE3F7700E09F51 /* vi-VN */, ); name = LaunchScreen.storyboard; sourceTree = ""; }; - C1005C46261071B5002F4F32 /* MainInterface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - C1005C47261071B5002F4F32 /* Base */, - ); - name = MainInterface.storyboard; - sourceTree = ""; - }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -430,6 +622,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -484,9 +677,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.com.linagora.ios.twake; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; ENABLE_BITCODE = NO; @@ -506,7 +701,7 @@ MARKETING_VERSION = 0.32.1; PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.twake; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = twake.development.profile; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = twake.development.profile; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -518,6 +713,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -573,6 +769,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -629,9 +826,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.com.linagora.ios.twake; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; ENABLE_BITCODE = NO; @@ -651,7 +850,7 @@ MARKETING_VERSION = 0.32.1; PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.twake; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = twake.development.profile; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = twake.development.profile; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -672,6 +871,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.com.linagora.ios.twake; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; ENABLE_BITCODE = NO; @@ -691,7 +891,7 @@ MARKETING_VERSION = 0.32.1; PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.twake; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = twake.distribution.profile; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = twake.distribution.profile; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -701,6 +901,7 @@ }; C1005C4E261071B5002F4F32 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 539142E4CD4E28C397AF0C3B /* Pods-Twake Share.debug.xcconfig */; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -708,27 +909,28 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "FluffyChat Share/FluffyChat Share.entitlements"; + CODE_SIGN_ENTITLEMENTS = "Twake Share/Twake Share.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.com.linagora.ios.twake; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = "FluffyChat Share/Info.plist"; + INFOPLIST_FILE = "Twake Share/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 2.3.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.twake.extension; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = twake.share.ext.development.profile; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = twake.share.ext.development.profile; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; @@ -740,6 +942,7 @@ }; C1005C4F261071B5002F4F32 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = BA9BBC5546D378D952097E1E /* Pods-Twake Share.release.xcconfig */; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -747,26 +950,27 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "FluffyChat Share/FluffyChat Share.entitlements"; + CODE_SIGN_ENTITLEMENTS = "Twake Share/Twake Share.entitlements"; CODE_SIGN_IDENTITY = "Apple Distribution"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Distribution"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.com.linagora.ios.twake; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = "FluffyChat Share/Info.plist"; + INFOPLIST_FILE = "Twake Share/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 2.3.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.twake.extension; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = twake.share.ext.distribution.profile; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = twake.share.ext.distribution.profile; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; @@ -776,6 +980,7 @@ }; C1005C50261071B5002F4F32 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 536699DF3AE0B9D3AF31677B /* Pods-Twake Share.profile.xcconfig */; buildSettings = { CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; @@ -783,26 +988,27 @@ CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "FluffyChat Share/FluffyChat Share.entitlements"; + CODE_SIGN_ENTITLEMENTS = "Twake Share/Twake Share.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + CUSTOM_GROUP_ID = group.com.linagora.ios.twake; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=iphoneos*]" = KUT463DS29; GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = "FluffyChat Share/Info.plist"; + INFOPLIST_FILE = "Twake Share/Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 14.4; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 2.3.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = com.linagora.ios.twake.extension; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PROVISIONING_PROFILE_SPECIFIER = twake.share.ext.development.profile; "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = twake.share.ext.development.profile; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; @@ -833,7 +1039,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - C1005C51261071B5002F4F32 /* Build configuration list for PBXNativeTarget "FluffyChat Share" */ = { + C1005C51261071B5002F4F32 /* Build configuration list for PBXNativeTarget "Twake Share" */ = { isa = XCConfigurationList; buildConfigurations = ( C1005C4E261071B5002F4F32 /* Debug */, diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index c34a50fe35..6f19754984 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + AppGroupId + $(CUSTOM_GROUP_ID) BGTaskSchedulerPermittedIdentifiers im.fluffychat.app diff --git a/ios/Runner/ar.lproj/Localizable.strings b/ios/Runner/ar.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/ar.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/ca.lproj/Localizable.strings b/ios/Runner/ca.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/ca.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/cs.lproj/Localizable.strings b/ios/Runner/cs.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/cs.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/de.lproj/Localizable.strings b/ios/Runner/de.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/de.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/en.lproj/Localizable.strings b/ios/Runner/en.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/en.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/eo.lproj/Localizable.strings b/ios/Runner/eo.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/eo.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/es.lproj/Localizable.strings b/ios/Runner/es.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/es.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/et.lproj/Localizable.strings b/ios/Runner/et.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/et.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/eu.lproj/Localizable.strings b/ios/Runner/eu.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/eu.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/fr.lproj/Localizable.strings b/ios/Runner/fr.lproj/Localizable.strings new file mode 100644 index 0000000000..debec9ea4c --- /dev/null +++ b/ios/Runner/fr.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "Vous avez un message chiffré"; diff --git a/ios/Runner/gl.lproj/Localizable.strings b/ios/Runner/gl.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/gl.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/hr-HR.lproj/Localizable.strings b/ios/Runner/hr-HR.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/hr-HR.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/hu-HU.lproj/Localizable.strings b/ios/Runner/hu-HU.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/hu-HU.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/hu.lproj/Localizable.strings b/ios/Runner/hu.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/hu.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/hy.lproj/Localizable.strings b/ios/Runner/hy.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/hy.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/it.lproj/Localizable.strings b/ios/Runner/it.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/it.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/ja.lproj/Localizable.strings b/ios/Runner/ja.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/ja.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/nb-NO.lproj/Localizable.strings b/ios/Runner/nb-NO.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/nb-NO.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/pl.lproj/Localizable.strings b/ios/Runner/pl.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/pl.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/pt.lproj/Localizable.strings b/ios/Runner/pt.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/pt.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/ru-RU.lproj/Localizable.strings b/ios/Runner/ru-RU.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/ru-RU.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/sk-SK.lproj/Localizable.strings b/ios/Runner/sk-SK.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/sk-SK.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/sv-SE.lproj/Localizable.strings b/ios/Runner/sv-SE.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/sv-SE.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/tr.lproj/Localizable.strings b/ios/Runner/tr.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/tr.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/uk.lproj/Localizable.strings b/ios/Runner/uk.lproj/Localizable.strings new file mode 100644 index 0000000000..18d97c2da4 --- /dev/null +++ b/ios/Runner/uk.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "You have 1 encrypted message"; diff --git a/ios/Runner/vi-VN.lproj/Localizable.strings b/ios/Runner/vi-VN.lproj/Localizable.strings new file mode 100644 index 0000000000..99ac00bb44 --- /dev/null +++ b/ios/Runner/vi-VN.lproj/Localizable.strings @@ -0,0 +1 @@ +"newMessageInTwake" = "Bạn có tin nhắn mới"; diff --git a/ios/FluffyChat Share/Info.plist b/ios/Twake Share/Info.plist similarity index 96% rename from ios/FluffyChat Share/Info.plist rename to ios/Twake Share/Info.plist index 922c4643fc..528bcbc4a3 100644 --- a/ios/FluffyChat Share/Info.plist +++ b/ios/Twake Share/Info.plist @@ -2,6 +2,8 @@ + AppGroupId + $(CUSTOM_GROUP_ID) CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName diff --git a/ios/FluffyChat Share/ShareViewController.swift b/ios/Twake Share/ShareViewController.swift similarity index 77% rename from ios/FluffyChat Share/ShareViewController.swift rename to ios/Twake Share/ShareViewController.swift index e56ab17539..11e5728d23 100644 --- a/ios/FluffyChat Share/ShareViewController.swift +++ b/ios/Twake Share/ShareViewController.swift @@ -5,8 +5,9 @@ import Photos class ShareViewController: SLComposeServiceViewController { // TODO: IMPORTANT: This should be your host app bundle identifier - let hostAppBundleIdentifier = "com.linagora.ios.twake" + var hostAppBundleIdentifier = "com.linagora.ios.twake" let sharedKey = "ShareKey" + var appGroupId = "" var sharedMedia: [SharedMediaFile] = [] var sharedText: [String] = [] let imageContentType = kUTTypeImage as String @@ -15,12 +16,30 @@ class ShareViewController: SLComposeServiceViewController { let urlContentType = kUTTypeURL as String let fileURLType = kUTTypeFileURL as String; + private func loadIds() { + // loading Share extension App Id + let shareExtensionAppBundleIdentifier = Bundle.main.bundleIdentifier!; + + + // convert ShareExtension id to host app id + // By default it is remove last part of id after last point + // For example: com.test.ShareExtension -> com.test + let lastIndexOfPoint = shareExtensionAppBundleIdentifier.lastIndex(of: "."); + hostAppBundleIdentifier = String(shareExtensionAppBundleIdentifier[.. + appGroupId = (Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as? String) ?? "group.\(hostAppBundleIdentifier)"; + } + override func isContentValid() -> Bool { return true } override func viewDidLoad() { super.viewDidLoad(); + + // load group and app id from build info + loadIds(); } override func viewDidAppear(_ animated: Bool) { @@ -64,7 +83,7 @@ class ShareViewController: SLComposeServiceViewController { // If this is the last item, save imagesData in userDefaults and redirect to host app if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + let userDefaults = UserDefaults(suiteName: this.appGroupId) userDefaults?.set(this.sharedText, forKey: this.sharedKey) userDefaults?.synchronize() this.redirectToHostApp(type: .text) @@ -85,7 +104,7 @@ class ShareViewController: SLComposeServiceViewController { // If this is the last item, save imagesData in userDefaults and redirect to host app if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + let userDefaults = UserDefaults(suiteName: this.appGroupId) userDefaults?.set(this.sharedText, forKey: this.sharedKey) userDefaults?.synchronize() this.redirectToHostApp(type: .text) @@ -98,30 +117,55 @@ class ShareViewController: SLComposeServiceViewController { } private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { + + func completed(copied: Bool, newPath: URL) { + if(copied) { + sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image)) + } + + // If this is the last item, save imagesData in userDefaults and redirect to host app + if index == (content.attachments?.count ?? 0) - 1 { + let userDefaults = UserDefaults(suiteName: "group.\(hostAppBundleIdentifier)") + userDefaults?.set(toData(data: sharedMedia), forKey: sharedKey) + userDefaults?.synchronize() + redirectToHostApp(type: .media) + } + } + attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in - if error == nil, let url = data as? URL, let this = self { - - // Always copy - let fileName = this.getFileName(from: url, type: .image) - let newPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! - .appendingPathComponent(fileName) - let copied = this.copyFile(at: url, to: newPath) - if(copied) { - this.sharedMedia.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image)) - } - - // If this is the last item, save imagesData in userDefaults and redirect to host app - if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") - userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) - userDefaults?.synchronize() - this.redirectToHostApp(type: .media) + guard let this = self else { return } + + if error == nil { + + let groupRootURL = FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! + + if let url = data as? URL { + // Always copy + let fileName = this.getFileName(from: url, type: .image) + let newPath = groupRootURL.appendingPathComponent(fileName) + let copied = this.copyFile(at: url, to: newPath) + completed(copied: copied, newPath: newPath) + } else if let image = data as? UIImage, + let jpegData = image.jpegData(compressionQuality: 1.0) { + + let fileName = "\(Int64(Date().timeIntervalSince1970)).jpg" + let newPath = groupRootURL.appendingPathComponent(fileName) + + var copied = false + do { + try jpegData.write(to: newPath) + copied = true + } catch { + this.dismissWithError() + } + completed(copied: copied, newPath: newPath) + } else { + this.dismissWithError() } - } else { - self?.dismissWithError() + this.dismissWithError() } } } @@ -134,7 +178,7 @@ class ShareViewController: SLComposeServiceViewController { // Always copy let fileName = this.getFileName(from: url, type: .video) let newPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! + .containerURL(forSecurityApplicationGroupIdentifier: this.appGroupId)! .appendingPathComponent(fileName) let copied = this.copyFile(at: url, to: newPath) if(copied) { @@ -146,7 +190,7 @@ class ShareViewController: SLComposeServiceViewController { // If this is the last item, save imagesData in userDefaults and redirect to host app if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + let userDefaults = UserDefaults(suiteName: this.appGroupId) userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) userDefaults?.synchronize() this.redirectToHostApp(type: .media) @@ -166,7 +210,7 @@ class ShareViewController: SLComposeServiceViewController { // Always copy let fileName = this.getFileName(from :url, type: .file) let newPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! + .containerURL(forSecurityApplicationGroupIdentifier: this.appGroupId)! .appendingPathComponent(fileName) let copied = this.copyFile(at: url, to: newPath) if (copied) { @@ -174,7 +218,7 @@ class ShareViewController: SLComposeServiceViewController { } if index == (content.attachments?.count)! - 1 { - let userDefaults = UserDefaults(suiteName: "group.\(this.hostAppBundleIdentifier)") + let userDefaults = UserDefaults(suiteName: this.appGroupId) userDefaults?.set(this.toData(data: this.sharedMedia), forKey: this.sharedKey) userDefaults?.synchronize() this.redirectToHostApp(type: .file) @@ -200,6 +244,8 @@ class ShareViewController: SLComposeServiceViewController { } private func redirectToHostApp(type: RedirectType) { + // ids may not loaded yet so we need loadIds here too + loadIds(); let url = URL(string: "ShareMedia-\(hostAppBundleIdentifier)://dataUrl=\(sharedKey)#\(type)") var responder = self as UIResponder? let selectorOpenURL = sel_registerName("openURL:") @@ -291,7 +337,7 @@ class ShareViewController: SLComposeServiceViewController { private func getThumbnailPath(for url: URL) -> URL { let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "") let path = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")! + .containerURL(forSecurityApplicationGroupIdentifier: appGroupId)! .appendingPathComponent("\(fileName).jpg") return path } @@ -332,4 +378,4 @@ extension Array { subscript (safe index: UInt) -> Element? { return Int(index) < count ? self[Int(index)] : nil } -} \ No newline at end of file +} diff --git a/ios/FluffyChat Share/FluffyChat Share.entitlements b/ios/Twake Share/Twake Share.entitlements similarity index 100% rename from ios/FluffyChat Share/FluffyChat Share.entitlements rename to ios/Twake Share/Twake Share.entitlements diff --git a/ios/en.lproj/Localizable.strings b/ios/en.lproj/Localizable.strings deleted file mode 100644 index d8d6570ee6..0000000000 --- a/ios/en.lproj/Localizable.strings +++ /dev/null @@ -1,8 +0,0 @@ -/* - Localizable.strings - Runner - - Created by Minh DrMinh on 7/19/23. - -*/ -"SINGLE_UNREAD" = "1 unread chat(s)"; diff --git a/lib/config/app_config.dart b/lib/config/app_config.dart index 5d4e844f01..c696c243d5 100644 --- a/lib/config/app_config.dart +++ b/lib/config/app_config.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'dart:ui'; -import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; @@ -44,7 +43,6 @@ abstract class AppConfig { static bool showDirectChatsInSpaces = true; static bool separateChatTypes = false; static bool autoplayImages = true; - static bool sendOnEnter = !PlatformInfos.isMobile; static bool experimentalVoip = false; static const bool hideTypingUsernames = false; static const bool hideAllStateEvents = false; diff --git a/lib/config/setting_keys.dart b/lib/config/setting_keys.dart index eeb3a16d64..2dca4a3172 100644 --- a/lib/config/setting_keys.dart +++ b/lib/config/setting_keys.dart @@ -26,6 +26,5 @@ abstract class SettingKeys { static const String dontAskForBootstrapKey = 'chat.fluffychat.dont_ask_bootstrap'; static const String autoplayImages = 'chat.fluffy.autoplay_images'; - static const String sendOnEnter = 'chat.fluffy.send_on_enter'; static const String experimentalVoip = 'chat.fluffy.experimental_voip'; } diff --git a/lib/di/global/get_it_initializer.dart b/lib/di/global/get_it_initializer.dart index cc728b5864..21ab9c6ae1 100644 --- a/lib/di/global/get_it_initializer.dart +++ b/lib/di/global/get_it_initializer.dart @@ -39,7 +39,6 @@ import 'package:fluffychat/domain/usecase/search/pre_search_recent_contacts_inte import 'package:fluffychat/domain/usecase/search/search_recent_chat_interactor.dart'; import 'package:fluffychat/domain/usecase/send_file_interactor.dart'; import 'package:fluffychat/domain/usecase/send_file_on_web_interactor.dart'; -import 'package:fluffychat/domain/usecase/send_image_interactor.dart'; import 'package:fluffychat/domain/usecase/send_images_interactor.dart'; import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dart'; import 'package:fluffychat/event/twake_event_dispatcher.dart'; @@ -141,7 +140,6 @@ class GetItInitializer { getIt.registerFactory( () => GetContactsInteractor(), ); - getIt.registerSingleton(SendImageInteractor()); getIt.registerSingleton(SendImagesInteractor()); getIt.registerSingleton( DownloadFileForPreviewInteractor(), diff --git a/lib/domain/model/preview_file/supported_preview_file_types.dart b/lib/domain/model/preview_file/supported_preview_file_types.dart index 18d280c924..4609d91c2d 100644 --- a/lib/domain/model/preview_file/supported_preview_file_types.dart +++ b/lib/domain/model/preview_file/supported_preview_file_types.dart @@ -29,6 +29,8 @@ class SupportedPreviewFileTypes { static const pdfMimeTypes = ['application/pdf', 'application/rtf']; + static const apkMimeTypes = ['application/vnd.android.package-archive']; + static const xlsMimeTypes = [ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.oasis.opendocument.spreadsheet', diff --git a/lib/domain/model/room/room_extension.dart b/lib/domain/model/room/room_extension.dart index b70f7ef3b5..313b66b4c8 100644 --- a/lib/domain/model/room/room_extension.dart +++ b/lib/domain/model/room/room_extension.dart @@ -45,4 +45,16 @@ extension RoomExtension on Room { Future unmute() async { await setPushRuleState(PushRuleState.notify); } + + String storePlaceholderFileInMem({ + required FileInfo fileInfo, + String? txid, + }) { + txid ??= client.generateUniqueTransactionId(); + final matrixFile = MatrixFile.fromFileInfo( + fileInfo: fileInfo, + ); + sendingFilePlaceholders[txid] = matrixFile; + return txid; + } } diff --git a/lib/domain/usecase/forward/forward_message_interactor.dart b/lib/domain/usecase/forward/forward_message_interactor.dart index 205e404a5b..e20ef4f7f3 100644 --- a/lib/domain/usecase/forward/forward_message_interactor.dart +++ b/lib/domain/usecase/forward/forward_message_interactor.dart @@ -2,6 +2,7 @@ import 'package:dartz/dartz.dart'; import 'package:fluffychat/app_state/failure.dart'; import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/domain/app_state/forward/forward_message_state.dart'; +import 'package:fluffychat/event/twake_event_types.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:matrix/matrix.dart'; @@ -37,7 +38,8 @@ class ForwardMessageInteractor { Room room, ) async* { final shareFile = message.tryGet('file'); - if (message.tryGet('msgtype') == 'chat.fluffy.shared_file' && + if (message.tryGet('msgtype') == + TwakeEventTypes.shareFileEventType && shareFile != null) { yield Right( ForwardMessageIsShareFileState(shareFile: shareFile, room: room), diff --git a/lib/domain/usecase/send_file_interactor.dart b/lib/domain/usecase/send_file_interactor.dart index 1c4006fedc..ac0291c0d6 100644 --- a/lib/domain/usecase/send_file_interactor.dart +++ b/lib/domain/usecase/send_file_interactor.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/data/network/extensions/file_info_extension.dart'; +import 'package:fluffychat/domain/model/room/room_extension.dart'; import 'package:fluffychat/presentation/extensions/send_file_extension.dart'; import 'package:matrix/matrix.dart'; @@ -14,9 +15,8 @@ class SendFileInteractor { }) async { try { for (final fileInfo in fileInfos) { - final txid = _storePlaceholderFileInMem( + final txid = room.storePlaceholderFileInMem( fileInfo: fileInfo, - room: room, ); await room.sendFileEvent( fileInfo, @@ -31,18 +31,4 @@ class SendFileInteractor { Logs().d("SendFileInteractor: execute(): $error"); } } - - String _storePlaceholderFileInMem({ - required Room room, - required FileInfo fileInfo, - }) { - final txid = room.client.generateUniqueTransactionId(); - final matrixFile = MatrixFile.fromMimeType( - name: fileInfo.fileName, - filePath: fileInfo.filePath, - mimeType: fileInfo.mimeType, - ); - room.sendingFilePlaceholders[txid] = matrixFile; - return txid; - } } diff --git a/lib/domain/usecase/send_image_interactor.dart b/lib/domain/usecase/send_image_interactor.dart deleted file mode 100644 index e45b7b7226..0000000000 --- a/lib/domain/usecase/send_image_interactor.dart +++ /dev/null @@ -1,30 +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 SendImageInteractor { - Future execute({ - required Room room, - required FileAssetEntity entity, - String? txId, - Event? inReplyTo, - String? editEventId, - int? shrinkImageMaxDimension, - Map? extraContent, - }) async { - final fileInfo = await entity.toFileInfo(); - if (fileInfo != null) { - try { - await room.sendFileEvent( - fileInfo, - txid: txId, - editEventId: editEventId, - inReplyTo: inReplyTo, - shrinkImageMaxDimension: shrinkImageMaxDimension, - ); - } catch (error) { - Logs().d("SendImageInteractor: execute(): $error"); - } - } - } -} diff --git a/lib/domain/usecase/send_images_interactor.dart b/lib/domain/usecase/send_images_interactor.dart index ee6c951262..fa8c7a4f3a 100644 --- a/lib/domain/usecase/send_images_interactor.dart +++ b/lib/domain/usecase/send_images_interactor.dart @@ -27,7 +27,7 @@ class SendImagesInteractor { ); } } catch (error) { - Logs().d("SendImageInteractor: execute(): $error"); + Logs().d("SendImagesInteractor: execute(): $error"); } } } diff --git a/lib/event/twake_event_types.dart b/lib/event/twake_event_types.dart new file mode 100644 index 0000000000..06e74a1415 --- /dev/null +++ b/lib/event/twake_event_types.dart @@ -0,0 +1,3 @@ +class TwakeEventTypes { + static const String shareFileEventType = 'app.twake.shared.file'; +} diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index b12c3f1057..340dad5c1f 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:adaptive_dialog/adaptive_dialog.dart'; +import 'package:debounce_throttle/debounce_throttle.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; @@ -22,7 +23,6 @@ import 'package:fluffychat/pages/chat/chat_room_search_mixin.dart'; import 'package:fluffychat/pages/chat/chat_view.dart'; import 'package:fluffychat/pages/chat/context_item_chat_action.dart'; import 'package:fluffychat/pages/chat/dialog_accept_invite_widget.dart'; -import 'package:fluffychat/pages/chat/event_info_dialog.dart'; import 'package:fluffychat/pages/chat/recording_dialog.dart'; import 'package:fluffychat/pages/chat_details/chat_details_actions_enum.dart'; import 'package:fluffychat/presentation/mixins/common_media_picker_mixin.dart'; @@ -31,6 +31,8 @@ import 'package:fluffychat/presentation/mixins/media_picker_mixin.dart'; import 'package:fluffychat/presentation/mixins/send_files_mixin.dart'; import 'package:fluffychat/presentation/model/forward/forward_argument.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; +import 'package:fluffychat/utils/clipboard.dart'; +import 'package:fluffychat/utils/dialog/twake_loading_dialog.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; @@ -47,7 +49,6 @@ import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; @@ -118,6 +119,9 @@ class ChatController extends State String? get roomId => widget.roomId; + final composerDebouncer = + Debouncer(const Duration(milliseconds: 100), initialValue: ''); + bool get isEmptyChat => timeline != null && !timeline!.events.any( @@ -261,7 +265,6 @@ class ChatController extends State if (!mounted) { return; } - hideKeyboardChatScreen(); hideSearchKeyboardIfNeeded(); setReadMarker(); if (!scrollController.hasClients) return; @@ -319,7 +322,9 @@ class ChatController extends State sendFileInteractor.execute( room: room!, fileInfos: [ - FileInfo(shareFile!.name, shareFile!.filePath!, shareFile!.size), + FileInfo.fromMatrixFile( + shareFile!, + ) ], ); } @@ -443,7 +448,7 @@ class ChatController extends State super.dispose(); } - TextEditingController sendController = TextEditingController(); + final TextEditingController sendController = TextEditingController(); void setSendingClient(Client? c) { // first cancle typing with the old sending client @@ -489,7 +494,7 @@ class ChatController extends State // ignore: unawaited_futures room!.sendTextEvent( - sendController.text, + sendController.text.trim(), inReplyTo: replyEvent, editEventId: editEvent?.eventId, parseCommands: parseCommands, @@ -521,6 +526,9 @@ class ChatController extends State void onFileTappedMobile(Event event) async { final permissionHandler = PermissionHandlerService(); + if (await permissionHandler.noNeedStoragePermission()) { + return _handleDownloadFileForPreviewMobile(event: event); + } final storagePermissionStatus = await permissionHandler.storagePermissionStatus; switch (storagePermissionStatus) { @@ -595,23 +603,16 @@ class ChatController extends State if (failure is DownloadFileForPreviewFailure) { TwakeSnackBar.show(context, 'Error: ${failure.exception}'); } + TwakeLoadingDialog.hideLoadingDialog(context); }, (success) { if (success is DownloadFileForPreviewSuccess) { _openDownloadedFileForPreview( downloadFileForPreviewResponse: success.downloadFileForPreviewResponse, ); - Navigator.of(context).pop(); + TwakeLoadingDialog.hideLoadingDialog(context); } else if (success is DownloadFileForPreviewLoading) { - showDialog( - context: context, - useRootNavigator: false, - builder: (BuildContext context) { - return const Center( - child: CircularProgressIndicator(), - ); - }, - ); + TwakeLoadingDialog.showLoadingDialog(context); } }); }); @@ -621,6 +622,11 @@ class ChatController extends State required DownloadFileForPreviewResponse downloadFileForPreviewResponse, }) async { final mimeType = downloadFileForPreviewResponse.mimeType; + if (Platform.isAndroid && + SupportedPreviewFileTypes.apkMimeTypes.contains(mimeType)) { + await Share.shareXFiles([XFile(downloadFileForPreviewResponse.filePath)]); + return; + } final openResults = await OpenFile.open( downloadFileForPreviewResponse.filePath, type: mimeType, @@ -777,8 +783,41 @@ class ChatController extends State return copyString; } + void copySingleEventAction() async { + if (selectedEvents.length == 1) { + final event = selectedEvents.first; + if (event.messageType == MessageTypes.Image && PlatformInfos.isWeb) { + final matrixFile = event.getMatrixFile() ?? + await event.downloadAndDecryptAttachment( + getThumbnail: true, + ); + try { + if (matrixFile.filePath != null) { + await Clipboard.instance.copyImageAsStream( + File(matrixFile.filePath!), + mimeType: event.mimeType, + ); + } else if (matrixFile.bytes != null) { + await Clipboard.instance.copyImageAsBytes( + matrixFile.bytes!, + mimeType: event.mimeType, + ); + } + } catch (e) { + TwakeSnackBar.show(context, L10n.of(context)!.copyImageFailed); + Logs().e( + 'copySingleEventAction(): failed to copy file ${matrixFile.name}', + ); + } + } else { + copyEventsAction(); + } + } + } + void copyEventsAction() { - Clipboard.setData(ClipboardData(text: _getSelectedEventString())); + Clipboard.instance.copyText(_getSelectedEventString()); + showEmojiPickerNotifier.value = false; setState(() { selectedEvents.clear(); @@ -1199,8 +1238,8 @@ class ChatController extends State } void onInputBarSubmitted(_) async { - await send(); await Future.delayed(const Duration(milliseconds: 100)); + await send(); FocusScope.of(context).requestFocus(inputFocus); } @@ -1222,7 +1261,7 @@ class ChatController extends State } } - void pinEvent() { + void pinEventAction() { final room = this.room; if (room == null) return; final pinnedEventIds = room.pinnedEventIds; @@ -1234,7 +1273,6 @@ class ChatController extends State } else { pinnedEventIds.addAll(selectedEventIds); } - clearSelectedEvents(); showFutureLoadingDialog( context: context, future: () => room.setPinnedEvents(pinnedEventIds), @@ -1287,9 +1325,6 @@ class ChatController extends State bool get isArchived => {Membership.leave, Membership.ban}.contains(room?.membership); - void showEventInfo([Event? event]) => - (event ?? selectedEvents.single).showInfoDialog(context); - void onPhoneButtonTap() async { // VoIP required Android SDK 21 if (PlatformInfos.isAndroid) { @@ -1469,11 +1504,11 @@ class ChatController extends State break; case ChatContextMenuActions.copyMessage: onSelectMessage(event); - copyEventsAction(); + copySingleEventAction(); break; case ChatContextMenuActions.pinMessage: onSelectMessage(event); - pinEvent(); + pinEventAction(); break; case ChatContextMenuActions.forward: onSelectMessage(event); @@ -1545,6 +1580,11 @@ class ChatController extends State } } + void actionWithClearSelections(Function action) { + action(); + clearSelectedEvents(); + } + @override Widget build(BuildContext context) { return ChatView(this, key: widget.key); diff --git a/lib/pages/chat/chat_app_bar_title.dart b/lib/pages/chat/chat_app_bar_title.dart index ee6fe20494..2fc56377fb 100644 --- a/lib/pages/chat/chat_app_bar_title.dart +++ b/lib/pages/chat/chat_app_bar_title.dart @@ -1,5 +1,6 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_title_style.dart'; +import 'package:fluffychat/utils/common_helper.dart'; import 'package:fluffychat/utils/room_status_extension.dart'; import 'package:fluffychat/utils/string_extension.dart'; import 'package:flutter/material.dart'; @@ -10,6 +11,7 @@ import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; +import 'package:rxdart/rxdart.dart'; class ChatAppBarTitle extends StatelessWidget { final Widget? actions; @@ -17,7 +19,7 @@ class ChatAppBarTitle extends StatelessWidget { final List selectedEvents; final bool isArchived; final TextEditingController sendController; - final Stream getStreamInstance; + final Stream connectivityResultStream; final VoidCallback onPushDetails; final String? roomName; @@ -29,7 +31,7 @@ class ChatAppBarTitle extends StatelessWidget { required this.selectedEvents, required this.isArchived, required this.sendController, - required this.getStreamInstance, + required this.connectivityResultStream, required this.onPushDetails, }) : super(key: key); @@ -98,7 +100,10 @@ class ChatAppBarTitle extends StatelessWidget { overflow: TextOverflow.ellipsis, style: ChatAppBarTitleStyle.appBarTitleStyle(context), ), - _buildStatusContent(context, room!), + _ChatAppBarStatusContent( + connectivityResultStream: connectivityResultStream, + room: room!, + ), ], ), ), @@ -106,62 +111,182 @@ class ChatAppBarTitle extends StatelessWidget { ), ); } +} - StreamBuilder _buildStatusContent( - BuildContext context, - Room room, - ) { - final TextStyle? statusTextStyle = - ChatAppBarTitleStyle.statusTextStyle(context); +class _ChatAppBarStatusContent extends StatelessWidget { + const _ChatAppBarStatusContent({ + required this.connectivityResultStream, + required this.room, + }); + + final Stream connectivityResultStream; + final Room room; + + @override + Widget build(BuildContext context) { + if (room.isDirectChat) { + return _DirectChatAppBarStatusContent( + connectivityResultStream: connectivityResultStream, + room: room, + ); + } + + return _GroupChatAppBarStatusContent( + connectivityResultStream: connectivityResultStream, + room: room, + ); + } +} + +class _DirectChatAppBarStatusContent extends StatelessWidget { + const _DirectChatAppBarStatusContent({ + required this.connectivityResultStream, + required this.room, + }); + + final Stream connectivityResultStream; + final Room room; + + @override + Widget build(BuildContext context) { + CachedPresence? directChatPresence = room.directChatPresence; + return FutureBuilder( + future: room.client.getPresence(room.directChatMatrixID!), + builder: (context, futureSnapshot) { + if (futureSnapshot.hasData) { + directChatPresence = CachedPresence.fromPresenceResponse( + futureSnapshot.data!, + room.directChatMatrixID!, + ); + } + return StreamBuilder( + stream: CombineLatestStream.list( + [connectivityResultStream, room.directChatPresenceStream], + ), + builder: (context, snapshot) { + final connectivityResult = tryCast( + snapshot.data?[0], + fallback: ConnectivityResult.none, + ); + directChatPresence = tryCast( + snapshot.data?[1], + fallback: directChatPresence, + ); + if (snapshot.hasData && + connectivityResult == ConnectivityResult.none) { + return _ChatAppBarTitleText(text: L10n.of(context)!.noConnection); + } + final typingText = room.getLocalizedTypingText(context); + if (typingText.isEmpty) { + return _ChatAppBarTitleText( + text: room + .getLocalizedStatus(context, presence: directChatPresence) + .capitalize(context), + ); + } else { + return _ChatAppBarTitleTyping(typingText: typingText); + } + }, + ); + }, + ); + } +} + +class _GroupChatAppBarStatusContent extends StatelessWidget { + const _GroupChatAppBarStatusContent({ + required this.connectivityResultStream, + required this.room, + }); + + final Stream connectivityResultStream; + final Room room; + + @override + Widget build(BuildContext context) { return StreamBuilder( - stream: getStreamInstance, + stream: connectivityResultStream, builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data == ConnectivityResult.none) { - return Text( - L10n.of(context)!.noConnection, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: statusTextStyle, - ); + final connectivityResult = tryCast( + snapshot.data, + fallback: ConnectivityResult.none, + ); + + if (snapshot.hasData && connectivityResult == ConnectivityResult.none) { + return _ChatAppBarTitleText(text: L10n.of(context)!.noConnection); } final typingText = room.getLocalizedTypingText(context); if (typingText.isEmpty) { - return Text( - room.getLocalizedStatus(context).capitalize(context), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: statusTextStyle, + return _ChatAppBarTitleText( + text: room.getLocalizedStatus(context).capitalize(context), ); } else { - return IntrinsicWidth( - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Expanded( - child: Text( - typingText, - maxLines: 1, - overflow: TextOverflow.clip, - style: statusTextStyle, - ), - ), - SizedBox( - width: 32, - height: 16, - child: Transform.translate( - offset: const Offset(0, -2), - child: LottieBuilder.asset( - 'assets/typing-indicator.zip', - fit: BoxFit.fitWidth, - width: 32, - ), - ), - ), - ], - ), - ); + return _ChatAppBarTitleTyping(typingText: typingText); } }, ); } } + +class _ChatAppBarTitleText extends StatelessWidget { + const _ChatAppBarTitleText({ + required this.text, + }); + + final String text; + + @override + Widget build(BuildContext context) { + final TextStyle? statusTextStyle = + ChatAppBarTitleStyle.statusTextStyle(context); + + return Text( + text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: statusTextStyle, + ); + } +} + +class _ChatAppBarTitleTyping extends StatelessWidget { + const _ChatAppBarTitleTyping({ + required this.typingText, + }); + + final String typingText; + + @override + Widget build(BuildContext context) { + final TextStyle? statusTextStyle = + ChatAppBarTitleStyle.statusTextStyle(context); + + return IntrinsicWidth( + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Text( + typingText, + maxLines: 1, + overflow: TextOverflow.clip, + style: statusTextStyle, + ), + ), + SizedBox( + width: 32, + height: 16, + child: Transform.translate( + offset: const Offset(0, -2), + child: LottieBuilder.asset( + 'assets/typing-indicator.zip', + fit: BoxFit.fitWidth, + width: 32, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat/chat_context_menu_actions.dart b/lib/pages/chat/chat_context_menu_actions.dart index cffc1e5d27..ae62a960ca 100644 --- a/lib/pages/chat/chat_context_menu_actions.dart +++ b/lib/pages/chat/chat_context_menu_actions.dart @@ -19,7 +19,7 @@ enum ChatContextMenuActions { case ChatContextMenuActions.forward: return L10n.of(context)!.forward; case ChatContextMenuActions.downloadFile: - return L10n.of(context)!.downloadFile; + return L10n.of(context)!.download; } } diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 54c46182e0..a4d848a99b 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/pages/chat/group_chat_empty_view.dart'; import 'package:fluffychat/pages/chat_draft/draft_chat_empty_view.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -59,7 +60,9 @@ class ChatEventList extends StatelessWidget { ), reverse: true, controller: controller.scrollController, - keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + keyboardDismissBehavior: PlatformInfos.isMobile + ? ScrollViewKeyboardDismissBehavior.manual + : ScrollViewKeyboardDismissBehavior.onDrag, childrenDelegate: SliverChildBuilderDelegate( (BuildContext context, int index) { // Footer to display typing indicator and read receipts: @@ -105,7 +108,6 @@ class ChatEventList extends StatelessWidget { event, onSwipe: (direction) => controller.replyAction(replyTo: event), - onInfoTab: controller.showEventInfo, onAvatarTab: (Event event) => showAdaptiveBottomSheet( context: context, builder: (c) => UserBottomSheet( diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index cdb2731d3e..87da3019cd 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -1,4 +1,3 @@ -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pages/chat/chat_input_row_mobile.dart'; import 'package:fluffychat/pages/chat/chat_input_row_style.dart'; import 'package:fluffychat/pages/chat/chat_input_row_web.dart'; @@ -13,7 +12,7 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:matrix/matrix.dart'; import 'chat.dart'; -import 'input_bar.dart'; +import 'input_bar/input_bar.dart'; class ChatInputRow extends StatelessWidget { final ChatController controller; @@ -156,7 +155,7 @@ class ChatInputRow extends StatelessWidget { maxLines: 8, autofocus: !PlatformInfos.isMobile, keyboardType: TextInputType.multiline, - textInputAction: AppConfig.sendOnEnter ? TextInputAction.send : null, + textInputAction: null, onSubmitted: controller.onInputBarSubmitted, focusNode: controller.inputFocus, keyboardFocusNode: controller.keyboardFocus, diff --git a/lib/pages/chat/chat_input_row_mobile.dart b/lib/pages/chat/chat_input_row_mobile.dart index 50d44e58bf..40d3166f26 100644 --- a/lib/pages/chat/chat_input_row_mobile.dart +++ b/lib/pages/chat/chat_input_row_mobile.dart @@ -1,6 +1,6 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/pages/chat/chat_input_row_style.dart'; -import 'package:fluffychat/pages/chat/input_bar.dart'; +import 'package:fluffychat/pages/chat/input_bar/input_bar.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/pages/chat/chat_input_row_web.dart b/lib/pages/chat/chat_input_row_web.dart index 6fdda0c808..136e80b7a0 100644 --- a/lib/pages/chat/chat_input_row_web.dart +++ b/lib/pages/chat/chat_input_row_web.dart @@ -1,6 +1,6 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/pages/chat/chat_input_row_style.dart'; -import 'package:fluffychat/pages/chat/input_bar.dart'; +import 'package:fluffychat/pages/chat/input_bar/input_bar.dart'; import 'package:fluffychat/widgets/twake_components/twake_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index e84b126f2e..0ef3a694a0 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/pages/chat/chat_event_list.dart'; import 'package:fluffychat/pages/chat/chat_loading_view.dart'; import 'package:fluffychat/pages/chat/chat_search_bottom_view.dart'; import 'package:fluffychat/pages/chat/chat_view_style.dart'; +import 'package:fluffychat/pages/chat/events/message_content_mixin.dart'; import 'package:fluffychat/pages/chat/pinned_events.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/pages/chat/tombstone_display.dart'; @@ -24,7 +25,7 @@ enum _EventContextAction { info, report } enum _RoomContextAction { search } -class ChatView extends StatelessWidget { +class ChatView extends StatelessWidget with MessageContentMixin { final ChatController controller; const ChatView(this.controller, {Key? key}) : super(key: key); @@ -37,29 +38,39 @@ class ChatView extends StatelessWidget { TwakeIconButton( icon: Icons.copy_outlined, tooltip: L10n.of(context)!.copy, - onTap: controller.copyEventsAction, + onTap: () => controller + .actionWithClearSelections(controller.copySingleEventAction), ), - if (controller.canRedactSelectedEvents) - TwakeIconButton( - icon: Icons.delete_outlined, - tooltip: L10n.of(context)!.redactMessage, - onTap: controller.redactEventsAction, - ), + // #777 Hide Delete Message functionality + // if (controller.canRedactSelectedEvents) + // TwakeIconButton( + // icon: Icons.delete_outlined, + // tooltip: L10n.of(context)!.redactMessage, + // onTap: () => controller + // .actionWithClearSelections(controller.redactEventsAction), + // ), TwakeIconButton( icon: Icons.push_pin_outlined, - onTap: controller.pinEvent, tooltip: L10n.of(context)!.pinMessage, + onTap: () => + controller.actionWithClearSelections(controller.pinEventAction), ), if (controller.selectedEvents.length == 1) PopupMenuButton<_EventContextAction>( onSelected: (action) { switch (action) { case _EventContextAction.info: - controller.showEventInfo(); - controller.clearSelectedEvents(); + controller.actionWithClearSelections( + () => showEventInfo( + context, + controller.selectedEvents.single, + ), + ); break; case _EventContextAction.report: - controller.reportEventAction(); + controller.actionWithClearSelections( + controller.reportEventAction, + ); break; } }, @@ -174,7 +185,7 @@ class ChatView extends StatelessWidget { room: controller.room, isArchived: controller.isArchived, sendController: controller.sendController, - getStreamInstance: controller + connectivityResultStream: controller .networkConnectionService .getStreamInstance(), actions: _appBarActions(context), @@ -206,7 +217,8 @@ class ChatView extends StatelessWidget { valueListenable: controller.showScrollDownButtonNotifier, builder: (context, showScrollDownButton, _) { if (showScrollDownButton && - controller.selectedEvents.isEmpty) { + controller.selectedEvents.isEmpty && + controller.replyEvent == null) { return Padding( padding: const EdgeInsets.only(bottom: 56.0), child: FloatingActionButton( diff --git a/lib/pages/chat/events/button_content.dart b/lib/pages/chat/events/button_content.dart new file mode 100644 index 0000000000..33dd298dcd --- /dev/null +++ b/lib/pages/chat/events/button_content.dart @@ -0,0 +1,61 @@ +import 'package:fluffychat/pages/chat/events/button_content_style.dart'; +import 'package:flutter/material.dart'; + +class ButtonContent extends StatelessWidget { + final Function onTap; + final IconData icon; + final String title; + + const ButtonContent({ + Key? key, + required this.onTap, + required this.icon, + required this.title, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Padding( + padding: ButtonContentStyle.parentPadding, + child: InkWell( + onTap: () => onTap, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onError, + shape: BoxShape.circle, + ), + padding: ButtonContentStyle.leadingIconPadding, + child: Icon( + icon, + color: Theme.of(context).colorScheme.primary, + size: ButtonContentStyle.leadingIconSize, + ), + ), + const SizedBox(width: ButtonContentStyle.leadingAndTextGap), + Container( + constraints: const BoxConstraints( + maxWidth: ButtonContentStyle.textMaxWidth, + ), + child: Text( + title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium!.copyWith( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/chat/events/encrypted_content_style.dart b/lib/pages/chat/events/button_content_style.dart similarity index 66% rename from lib/pages/chat/events/encrypted_content_style.dart rename to lib/pages/chat/events/button_content_style.dart index cbde22376f..44c6ae8639 100644 --- a/lib/pages/chat/events/encrypted_content_style.dart +++ b/lib/pages/chat/events/button_content_style.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -class EncryptedContentStyle { - static const EdgeInsets parentPadding = EdgeInsets.symmetric( - horizontal: 8, +class ButtonContentStyle { + static const EdgeInsets parentPadding = EdgeInsets.only( + left: 2, + right: 8, ); static const EdgeInsets leadingIconPadding = EdgeInsets.all(5); static const double leadingIconSize = 20; diff --git a/lib/pages/chat/events/call_invite_content.dart b/lib/pages/chat/events/call_invite_content.dart new file mode 100644 index 0000000000..3ef73870f1 --- /dev/null +++ b/lib/pages/chat/events/call_invite_content.dart @@ -0,0 +1,28 @@ +import 'package:fluffychat/pages/chat/events/button_content.dart'; +import 'package:fluffychat/pages/chat/events/message_content_mixin.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class CallInviteContent extends StatelessWidget with MessageContentMixin { + final Event event; + + const CallInviteContent({Key? key, required this.event}) : super(key: key); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: event.fetchSenderUser(), + builder: (context, snapshot) { + return ButtonContent( + title: L10n.of(context)!.startedACall( + snapshot.data?.calcDisplayname() ?? + event.senderFromMemoryOrFallback.calcDisplayname(), + ), + icon: Icons.phone_outlined, + onTap: () => showEventInfo(context, event), + ); + }, + ); + } +} diff --git a/lib/pages/chat/events/encrypted_content.dart b/lib/pages/chat/events/encrypted_content.dart index dd8dfbaa3c..f8c636ccb3 100644 --- a/lib/pages/chat/events/encrypted_content.dart +++ b/lib/pages/chat/events/encrypted_content.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/pages/chat/events/encrypted_content_style.dart'; +import 'package:fluffychat/pages/chat/events/button_content.dart'; import 'package:fluffychat/pages/chat/events/encrypted_mixin.dart'; import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; @@ -11,46 +11,10 @@ class EncryptedContent extends StatelessWidget with EncryptedMixin { @override Widget build(BuildContext context) { - return Padding( - padding: EncryptedContentStyle.parentPadding, - child: InkWell( - onTap: () => verifyOrRequestKey(context, event), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.onError, - shape: BoxShape.circle, - ), - padding: EncryptedContentStyle.leadingIconPadding, - child: Icon( - Icons.lock, - color: Theme.of(context).colorScheme.primary, - size: EncryptedContentStyle.leadingIconSize, - ), - ), - const SizedBox(width: EncryptedContentStyle.leadingAndTextGap), - Container( - constraints: const BoxConstraints( - maxWidth: EncryptedContentStyle.textMaxWidth, - ), - child: Text( - maxLines: 2, - L10n.of(context)!.thisMessageHasBeenEncrypted, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - ], - ), - ], - ), - ), + return ButtonContent( + onTap: () => verifyOrRequestKey(context, event), + icon: Icons.lock, + title: L10n.of(context)!.thisMessageHasBeenEncrypted, ); } } diff --git a/lib/pages/chat/events/message/message.dart b/lib/pages/chat/events/message/message.dart index 49184d1d8a..a674954f96 100644 --- a/lib/pages/chat/events/message/message.dart +++ b/lib/pages/chat/events/message/message.dart @@ -31,7 +31,6 @@ class Message extends StatelessWidget { final Event? nextEvent; final void Function(Event)? onSelect; final void Function(Event)? onAvatarTab; - final void Function(Event)? onInfoTab; final void Function(String)? scrollToEventId; final void Function(SwipeDirection) onSwipe; final void Function(bool, Event)? onHover; @@ -49,7 +48,6 @@ class Message extends StatelessWidget { this.nextEvent, this.longPressSelect = false, this.onSelect, - this.onInfoTab, this.onAvatarTab, this.onHover, this.scrollToEventId, @@ -344,8 +342,6 @@ class Message extends StatelessWidget { displayEvent, textColor: textColor, - onInfoTab: - onInfoTab, endOfBubbleWidget: Padding( padding: diff --git a/lib/pages/chat/events/message_content.dart b/lib/pages/chat/events/message_content.dart index c87288ee85..913db51949 100644 --- a/lib/pages/chat/events/message_content.dart +++ b/lib/pages/chat/events/message_content.dart @@ -2,10 +2,13 @@ import 'package:fluffychat/app_state/success.dart'; import 'package:fluffychat/domain/app_state/room/chat_room_search_state.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pages/chat/events/call_invite_content.dart'; import 'package:fluffychat/pages/chat/events/encrypted_content.dart'; import 'package:fluffychat/pages/chat/events/message_content_style.dart'; +import 'package:fluffychat/pages/chat/events/redacted_content.dart'; import 'package:fluffychat/pages/chat/events/sending_image_info_widget.dart'; import 'package:fluffychat/pages/chat/events/sending_video_widget.dart'; +import 'package:fluffychat/pages/chat/events/unknown_content.dart'; import 'package:fluffychat/presentation/mixins/play_video_action_mixin.dart'; import 'package:fluffychat/presentation/model/file/display_image_info.dart'; import 'package:fluffychat/utils/extension/image_size_extension.dart'; @@ -22,7 +25,6 @@ import 'package:matrix/matrix.dart' hide Visibility; import 'package:fluffychat/pages/chat/events/event_video_player.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/event_extension.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import 'audio_player.dart'; import 'cute_events.dart'; import 'html_message.dart'; @@ -34,7 +36,6 @@ import 'sticker.dart'; class MessageContent extends StatelessWidget with PlayVideoActionMixin { final Event event; final Color textColor; - final void Function(Event)? onInfoTab; final Widget endOfBubbleWidget; final Color backgroundColor; final void Function()? onTapPreview; @@ -44,7 +45,6 @@ class MessageContent extends StatelessWidget with PlayVideoActionMixin { const MessageContent( this.event, { - this.onInfoTab, Key? key, required this.controller, required this.textColor, @@ -58,8 +58,6 @@ class MessageContent extends StatelessWidget with PlayVideoActionMixin { @override Widget build(BuildContext context) { final fontSize = AppConfig.messageFontSize * AppConfig.fontSizeFactor; - final buttonTextColor = - event.senderId == Matrix.of(context).client.userID ? textColor : null; switch (event.type) { case EventTypes.Encrypted: return EncryptedContent(event: event); @@ -220,25 +218,9 @@ class MessageContent extends StatelessWidget with PlayVideoActionMixin { textmessage: default: if (event.redacted) { - return FutureBuilder( - future: event.redactedBecause?.fetchSenderUser(), - builder: (context, snapshot) { - return _ButtonContent( - label: L10n.of(context)!.redactedAnEvent( - snapshot.data?.calcDisplayname() ?? - event.senderFromMemoryOrFallback.calcDisplayname(), - ), - icon: const Icon(Icons.delete_outlined), - textColor: buttonTextColor, - onPressed: () => onInfoTab!(event), - ); - }, - ); + return RedactedContent(event: event); } - final bigEmotes = event.onlyEmotes && - event.numberEmotes > 0 && - event.numberEmotes <= 10; return FutureBuilder( future: event.calcLocalizedBody( MatrixLocals(L10n.of(context)!), @@ -263,11 +245,10 @@ class MessageContent extends StatelessWidget with PlayVideoActionMixin { Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context).colorScheme.onSurface, ), - linkStyle: TextStyle( - color: Theme.of(context).colorScheme.secondary, - fontSize: bigEmotes ? fontSize * 3 : fontSize, - decorationColor: textColor.withAlpha(150), - ), + linkStyle: + Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.secondary, + ), childWidget: Visibility( visible: false, maintainSize: true, @@ -297,68 +278,13 @@ class MessageContent extends StatelessWidget with PlayVideoActionMixin { ); } case EventTypes.CallInvite: - return FutureBuilder( - future: event.fetchSenderUser(), - builder: (context, snapshot) { - return _ButtonContent( - label: L10n.of(context)!.startedACall( - snapshot.data?.calcDisplayname() ?? - event.senderFromMemoryOrFallback.calcDisplayname(), - ), - icon: const Icon(Icons.phone_outlined), - textColor: buttonTextColor, - onPressed: () => onInfoTab!(event), - ); - }, - ); + return CallInviteContent(event: event); default: - return FutureBuilder( - future: event.fetchSenderUser(), - builder: (context, snapshot) { - return _ButtonContent( - label: L10n.of(context)!.userSentUnknownEvent( - snapshot.data?.calcDisplayname() ?? - event.senderFromMemoryOrFallback.calcDisplayname(), - event.type, - ), - icon: const Icon(Icons.info_outlined), - textColor: buttonTextColor, - onPressed: () => onInfoTab!(event), - ); - }, - ); + return UnknownContent(event: event); } } } -class _ButtonContent extends StatelessWidget { - final void Function() onPressed; - final String label; - final Icon icon; - final Color? textColor; - - const _ButtonContent({ - required this.label, - required this.icon, - required this.textColor, - required this.onPressed, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return OutlinedButton.icon( - onPressed: onPressed, - icon: icon, - label: Text(label, overflow: TextOverflow.ellipsis), - style: OutlinedButton.styleFrom( - foregroundColor: textColor, - backgroundColor: MessageContentStyle.backgroundColorButton, - ), - ); - } -} - class _MessageImageBuilder extends StatelessWidget { final Event event; @@ -386,6 +312,7 @@ class _MessageImageBuilder extends StatelessWidget { file.height?.toDouble() ?? MessageContentStyle.imageHeight(context), ).getDisplayImageInfo(context); return SendingImageInfoWidget( + key: ValueKey(event.eventId), matrixFile: file, event: event, onTapPreview: onTapPreview, @@ -457,10 +384,12 @@ class _MessageVideoBuilder extends StatelessWidget { } if (isSendingVideo(matrixFile)) { final file = matrixFile as MatrixVideoFile; - displayImageInfo = Size( - file.width!.toDouble(), - file.height!.toDouble(), - ).getDisplayImageInfo(context); + if (file.width != null && file.height != null) { + displayImageInfo = Size( + file.width!.toDouble(), + file.height!.toDouble(), + ).getDisplayImageInfo(context); + } return SendingVideoWidget( key: ValueKey(event.eventId), event: event, @@ -483,8 +412,6 @@ class _MessageVideoBuilder extends StatelessWidget { } bool isSendingVideo(MatrixFile? matrixFile) { - return matrixFile is MatrixVideoFile && - matrixFile.width != null && - matrixFile.height != null; + return matrixFile is MatrixVideoFile && matrixFile.bytes != null; } } diff --git a/lib/pages/chat/events/message_content_mixin.dart b/lib/pages/chat/events/message_content_mixin.dart new file mode 100644 index 0000000000..2e0ed86d82 --- /dev/null +++ b/lib/pages/chat/events/message_content_mixin.dart @@ -0,0 +1,8 @@ +import 'package:fluffychat/pages/chat/event_info_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +mixin MessageContentMixin { + void showEventInfo(BuildContext context, Event event) => + event.showInfoDialog(context); +} diff --git a/lib/pages/chat/events/redacted_content.dart b/lib/pages/chat/events/redacted_content.dart new file mode 100644 index 0000000000..635665c537 --- /dev/null +++ b/lib/pages/chat/events/redacted_content.dart @@ -0,0 +1,25 @@ +import 'package:fluffychat/pages/chat/events/button_content.dart'; +import 'package:fluffychat/pages/chat/events/message_content_mixin.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class RedactedContent extends StatelessWidget with MessageContentMixin { + final Event event; + + const RedactedContent({Key? key, required this.event}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ButtonContent( + title: event.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + withSenderNamePrefix: false, + hideReply: true, + ), + icon: Icons.delete_outlined, + onTap: () => showEventInfo(context, event), + ); + } +} diff --git a/lib/pages/chat/events/unknown_content.dart b/lib/pages/chat/events/unknown_content.dart new file mode 100644 index 0000000000..8f0c6f0971 --- /dev/null +++ b/lib/pages/chat/events/unknown_content.dart @@ -0,0 +1,29 @@ +import 'package:fluffychat/pages/chat/events/button_content.dart'; +import 'package:fluffychat/pages/chat/events/message_content_mixin.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class UnknownContent extends StatelessWidget with MessageContentMixin { + final Event event; + + const UnknownContent({Key? key, required this.event}) : super(key: key); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: event.fetchSenderUser(), + builder: (context, snapshot) { + return ButtonContent( + title: L10n.of(context)!.userSentUnknownEvent( + snapshot.data?.calcDisplayname() ?? + event.senderFromMemoryOrFallback.calcDisplayname(), + event.type, + ), + icon: Icons.info_outlined, + onTap: () => showEventInfo(context, event), + ); + }, + ); + } +} diff --git a/lib/pages/chat/input_bar/context_menu_input_bar.dart b/lib/pages/chat/input_bar/context_menu_input_bar.dart new file mode 100644 index 0000000000..ace4e0c3cd --- /dev/null +++ b/lib/pages/chat/input_bar/context_menu_input_bar.dart @@ -0,0 +1,82 @@ +import 'package:fluffychat/presentation/enum/chat/popup_menu_item_web_enum.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:fluffychat/presentation/extensions/text_editting_controller_extension.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:matrix/matrix.dart'; + +class ContextMenuInputBar extends StatelessWidget { + final Widget child; + + final TextEditingController? controller; + + final VoidCallback handlePaste; + + final Room? room; + + const ContextMenuInputBar({ + super.key, + required this.child, + required this.handlePaste, + this.controller, + this.room, + }); + + @override + Widget build(BuildContext context) { + return Listener( + onPointerDown: (PointerDownEvent event) async { + if (event.kind == PointerDeviceKind.mouse && + event.buttons == kSecondaryMouseButton) { + // FIXME: the contextMenuBuilder.editable can do this but its style in web is not customizable + // currently this is only solution + final screenSize = MediaQuery.of(context).size; + final offset = event.position; + final position = RelativeRect.fromLTRB( + offset.dx, + offset.dy, + screenSize.width - offset.dx, + screenSize.height - offset.dy, + ); + final menuItem = await showMenu( + useRootNavigator: PlatformInfos.isWeb, + context: context, + items: [ + PopupMenuItem( + value: InputBarContextMenu.copy, + child: Text(L10n.of(context)!.copy), + ), + PopupMenuItem( + value: InputBarContextMenu.cut, + child: Text(L10n.of(context)!.cut), + ), + PopupMenuItem( + value: InputBarContextMenu.paste, + child: Text(L10n.of(context)!.paste), + ), + ], + position: position, + ); + + if (menuItem == null || controller == null) { + return; + } + + switch (menuItem) { + case InputBarContextMenu.copy: + controller!.copyText(); + break; + case InputBarContextMenu.cut: + controller!.cutText(); + break; + case InputBarContextMenu.paste: + handlePaste(); + break; + } + } + }, + child: child, + ); + } +} diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar/input_bar.dart similarity index 76% rename from lib/pages/chat/input_bar.dart rename to lib/pages/chat/input_bar/input_bar.dart index bce81fe17c..afd418bd97 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar/input_bar.dart @@ -1,7 +1,12 @@ -import 'package:fluffychat/utils/extension/raw_key_event_extension.dart'; +import 'package:fluffychat/pages/chat/command_hints.dart'; +import 'package:fluffychat/pages/chat/input_bar/context_menu_input_bar.dart'; +import 'package:fluffychat/pages/chat/input_bar/input_bar_shortcut.dart'; +import 'package:fluffychat/presentation/mixins/paste_image_mixin.dart'; +import 'package:fluffychat/utils/clipboard.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; +import 'package:fluffychat/presentation/extensions/text_editting_controller_extension.dart'; import 'package:emojis/emoji.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -9,13 +14,10 @@ import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:matrix/matrix.dart'; import 'package:slugify/slugify.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/mxc_image.dart'; -import 'command_hints.dart'; - -class InputBar extends StatelessWidget { +class InputBar extends StatelessWidget with PasteImageMixin { final Room? room; final int? minLines; final int? maxLines; @@ -30,7 +32,7 @@ class InputBar extends StatelessWidget { final bool? autofocus; final bool readOnly; - const InputBar({ + InputBar({ this.room, this.minLines, this.maxLines, @@ -299,64 +301,113 @@ class InputBar extends StatelessWidget { } } + Future handlePaste(BuildContext context) async { + if (await Clipboard.instance.isReadableImageFormat() && room != null) { + await pasteImage(context, room!); + } else { + await controller?.pasteText(); + } + } + @override Widget build(BuildContext context) { - final useShortCuts = (PlatformInfos.isWeb || - PlatformInfos.isDesktop || - AppConfig.sendOnEnter); - return RawKeyboardListener( - focusNode: keyboardFocusNode, - onKey: (event) { - if (useShortCuts && event.isEnter) { - onSubmitted?.call(controller?.text ?? ''); - } - }, - child: TypeAheadField>( - direction: AxisDirection.up, - hideOnEmpty: true, - hideOnLoading: true, - keepSuggestionsOnSuggestionSelected: true, - debounceDuration: const Duration(milliseconds: 50), - // show suggestions after 50ms idle time (default is 300) - textFieldConfiguration: TextFieldConfiguration( - minLines: minLines, - maxLines: maxLines, - keyboardType: keyboardType!, - textInputAction: textInputAction, - autofocus: autofocus!, - style: InputBarStyle.getTypeAheadTextStyle(context), - onSubmitted: (text) { - // fix for library for now - // it sets the types for the callback incorrectly - onSubmitted!(text); - }, - controller: controller, - decoration: decoration!, - focusNode: focusNode, - onChanged: (text) { - // fix for the library for now - // it sets the types for the callback incorrectly - onChanged!(text); - }, - textCapitalization: TextCapitalization.sentences, - ), - suggestionsCallback: getSuggestions, - itemBuilder: (context, suggestion) => SuggestionTile( - suggestion: suggestion, - client: Matrix.of(context).client, + return InputBarShortcuts( + controller: controller, + room: room, + onSubmitted: onSubmitted, + handlePaste: () => handlePaste(context), + child: ContextMenuInputBar( + handlePaste: () => handlePaste(context), + child: TypeAheadField>( + direction: AxisDirection.up, + hideOnEmpty: true, + hideOnLoading: true, + keepSuggestionsOnSuggestionSelected: true, + debounceDuration: const Duration(milliseconds: 50), + // show suggestions after 50ms idle time (default is 300) + textFieldConfiguration: TextFieldConfiguration( + minLines: minLines, + maxLines: maxLines, + keyboardType: keyboardType!, + textInputAction: textInputAction, + autofocus: autofocus!, + style: InputBarStyle.getTypeAheadTextStyle(context), + controller: controller, + decoration: decoration!, + focusNode: focusNode, + onChanged: (text) { + // fix for the library for now + // it sets the types for the callback incorrectly + onChanged!(text); + }, + onSubmitted: PlatformInfos.isMobile + ? (text) { + // fix for library for now + // it sets the types for the callback incorrectly + onSubmitted!(text); + } + : null, + contextMenuBuilder: !PlatformInfos.isWeb + ? ( + BuildContext contextMenucontext, + EditableTextState editableTextState, + ) { + return AdaptiveTextSelectionToolbar.editable( + anchors: editableTextState.contextMenuAnchors, + clipboardStatus: ClipboardStatus.pasteable, + onPaste: !PlatformInfos.isWeb + ? () async { + if (room == null) { + // FIXME: need to handle the case when in draft chat + return; + } + editableTextState + .pasteText(SelectionChangedCause.toolbar); + } + : null, + onCopy: () { + editableTextState + .copySelection(SelectionChangedCause.toolbar); + }, + onCut: () { + editableTextState + .cutSelection(SelectionChangedCause.toolbar); + }, + onSelectAll: () { + editableTextState + .selectAll(SelectionChangedCause.toolbar); + }, + ); + } + : null, + textCapitalization: TextCapitalization.sentences, + ), + suggestionsCallback: getSuggestions, + itemBuilder: (context, suggestion) => SuggestionTile( + suggestion: suggestion, + client: Matrix.of(context).client, + ), + onSuggestionSelected: (Map suggestion) => + insertSuggestion(context, suggestion), + errorBuilder: (BuildContext context, Object? error) => Container(), + loadingBuilder: (BuildContext context) => Container(), + // fix loading briefly flickering a dark box + noItemsFoundBuilder: (BuildContext context) => + Container(), // fix loading briefly showing no suggestions ), - onSuggestionSelected: (Map suggestion) => - insertSuggestion(context, suggestion), - errorBuilder: (BuildContext context, Object? error) => Container(), - loadingBuilder: (BuildContext context) => Container(), - // fix loading briefly flickering a dark box - noItemsFoundBuilder: (BuildContext context) => - Container(), // fix loading briefly showing no suggestions ), ); } } +class PasteIntent extends Intent { + const PasteIntent(); +} + +class CopyIntent extends Intent { + const CopyIntent(); +} + class NewLineIntent extends Intent {} class SubmitLineIntent extends Intent {} diff --git a/lib/pages/chat/input_bar/input_bar_shortcut.dart b/lib/pages/chat/input_bar/input_bar_shortcut.dart new file mode 100644 index 0000000000..25ef2fe959 --- /dev/null +++ b/lib/pages/chat/input_bar/input_bar_shortcut.dart @@ -0,0 +1,64 @@ +import 'package:fluffychat/presentation/extensions/text_editting_controller_extension.dart'; +import 'package:fluffychat/utils/one_time_debouncer.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' as flutter; +import 'package:matrix/matrix.dart'; + +class InputBarShortcuts extends StatelessWidget { + final Widget child; + + final TextEditingController? controller; + + final Room? room; + + final VoidCallback handlePaste; + + final ValueChanged? onSubmitted; + + InputBarShortcuts({ + super.key, + required this.child, + required this.handlePaste, + this.room, + this.controller, + this.onSubmitted, + }); + + final _debouncer = OneTimeDebouncer(milliseconds: 50); + + @override + Widget build(BuildContext context) { + return CallbackShortcuts( + bindings: { + SingleActivator( + flutter.LogicalKeyboardKey.keyV, + meta: PlatformInfos.isMacKeyboardPlatform, + control: !PlatformInfos.isMacKeyboardPlatform, + ): () async { + handlePaste(); + }, + const SingleActivator( + flutter.LogicalKeyboardKey.keyC, + meta: true, + ): () { + controller?.copyText(); + }, + const SingleActivator( + flutter.LogicalKeyboardKey.enter, + shift: true, + ): () { + _debouncer.run(() { + controller?.addNewLine(); + }); + }, + const SingleActivator( + flutter.LogicalKeyboardKey.enter, + ): () { + onSubmitted?.call(controller?.text ?? ''); + } + }, + child: child, + ); + } +} diff --git a/lib/pages/chat/send_file_dialog.dart b/lib/pages/chat/send_file_dialog.dart index 42bf1949e9..0541f60f1f 100644 --- a/lib/pages/chat/send_file_dialog.dart +++ b/lib/pages/chat/send_file_dialog.dart @@ -1,3 +1,6 @@ +import 'package:fluffychat/presentation/extensions/send_file_web_extension.dart'; +import 'package:fluffychat/utils/localized_exception_extension.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -43,18 +46,16 @@ class SendFileDialogState extends State { } // ignore: unused_local_variable final scaffoldMessenger = ScaffoldMessenger.of(context); - // widget.room - // .sendFileEvent( - // file, - // thumbnail: thumbnail, - // shrinkImageMaxDimension: origImage ? null : 1600, - // ) - // .catchError((e) { - // scaffoldMessenger.showSnackBar( - // SnackBar(content: Text((e as Object).toLocalizedString(context))), - // ); - // return null; - // }); + widget.room + .sendFileOnWebEvent( + file, + thumbnail: thumbnail, + shrinkImageMaxDimension: origImage ? null : 1600, + ) + .catchError((e) { + TwakeSnackBar.show(context, (e as Object).toLocalizedString(context)); + return null; + }); } Navigator.of(context, rootNavigator: false).pop(); @@ -91,18 +92,6 @@ class SendFileDialogState extends State { fit: BoxFit.contain, ), ), - Row( - children: [ - Checkbox( - value: origImage, - onChanged: (v) => setState(() => origImage = v ?? false), - ), - InkWell( - onTap: () => setState(() => origImage = !origImage), - child: Text('${L10n.of(context)!.sendOriginal} ($sizeString)'), - ), - ], - ) ], ); } else { diff --git a/lib/pages/chat_draft/draft_chat_view.dart b/lib/pages/chat_draft/draft_chat_view.dart index 7d4b2e7d71..fd8cd8f5de 100644 --- a/lib/pages/chat_draft/draft_chat_view.dart +++ b/lib/pages/chat_draft/draft_chat_view.dart @@ -1,11 +1,10 @@ import 'package:animations/animations.dart'; import 'package:emoji_picker_flutter/emoji_picker_flutter.dart'; -import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pages/chat/chat_app_bar_title_style.dart'; import 'package:fluffychat/pages/chat/chat_input_row_style.dart'; -import 'package:fluffychat/pages/chat/input_bar.dart'; +import 'package:fluffychat/pages/chat/input_bar/input_bar.dart'; import 'package:fluffychat/pages/chat_draft/draft_chat.dart'; import 'package:fluffychat/pages/chat_draft/draft_chat_empty_view.dart'; import 'package:fluffychat/pages/chat_draft/draft_chat_view_style.dart'; @@ -122,9 +121,7 @@ class DraftChatView extends StatelessWidget { DraftChatViewStyle.maxLinesInputBar, autofocus: !PlatformInfos.isMobile, keyboardType: TextInputType.multiline, - textInputAction: AppConfig.sendOnEnter - ? TextInputAction.send - : null, + textInputAction: null, onSubmitted: controller.onInputBarSubmitted, focusNode: controller.inputFocus, diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 0a477d39f3..d306111d63 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -5,20 +5,16 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/model/recovery_words/recovery_words.dart'; -import 'package:fluffychat/domain/model/room/room_extension.dart'; import 'package:fluffychat/domain/usecase/recovery/get_recovery_words_interactor.dart'; import 'package:fluffychat/mixin/comparable_presentation_contact_mixin.dart'; import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; import 'package:fluffychat/pages/bootstrap/tom_bootstrap_dialog.dart'; import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; -import 'package:fluffychat/pages/chat_list/receive_sharing_intent_mixin.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_security/settings_security.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; import 'package:fluffychat/presentation/extensions/client_extension.dart'; import 'package:fluffychat/presentation/model/chat_list/chat_selection_actions.dart'; import 'package:fluffychat/utils/extension/build_context_extension.dart'; -import 'package:fluffychat/utils/famedlysdk_store.dart'; -import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/utils/tor_stub.dart' @@ -60,125 +56,113 @@ class ChatList extends StatefulWidget { class ChatListController extends State with + AutomaticKeepAliveClientMixin, TickerProviderStateMixin, RouteAware, ComparablePresentationContactMixin, PopupContextMenuActionMixin, - PopupMenuWidgetMixin, - ReceiveSharingIntentMixin { + PopupMenuWidgetMixin { final _getRecoveryWordsInteractor = getIt.get(); - bool get displayNavigationBar => false; - final responsive = getIt.get(); + final ValueNotifier expandRoomsForAllNotifier = ValueNotifier(true); + + final ValueNotifier expandRoomsForPinNotifier = ValueNotifier(true); + + final ValueNotifier selectModeNotifier = + ValueNotifier(SelectMode.normal); + + final ValueNotifier> + conversationSelectionNotifier = ValueNotifier([]); + + final TextEditingController searchChatController = TextEditingController(); + + final ScrollController scrollController = ScrollController(); + + final StreamController _clientStream = StreamController.broadcast(); + String? activeSpaceId; - Client get client => Matrix.of(context).client; + Future? publicRoomsResponse; - void resetActiveSpaceId() { - setState(() { - activeSpaceId = null; - }); - } + SearchUserDirectoryResponse? userSearchResult; - void setActiveSpace(String? spaceId) { - setState(() { - activeSpaceId = spaceId; - }); - } + QueryPublicRoomsResponse? roomSearchResult; + + bool isTorBrowser = false; + + bool waitForFirstSync = false; + + bool scrolledToTop = true; + + Client get client => Matrix.of(context).client; ActiveFilter activeFilter = AppConfig.separateChatTypes ? ActiveFilter.messages : ActiveFilter.allChats; + List get _filteredRooms => client.filteredRoomsForAll(activeFilter); + List get filteredRoomsForAll => - Matrix.of(context).client.filteredRoomsForAll(activeFilter); + _filteredRooms.where((room) => !room.isFavourite).toList(); - bool isSearchMode = false; - Future? publicRoomsResponse; - String? searchServer; - Timer? _coolDown; - SearchUserDirectoryResponse? userSearchResult; - QueryPublicRoomsResponse? roomSearchResult; + List get filteredRoomsForPin => + _filteredRooms.where((room) => room.isFavourite).toList(); - final ValueNotifier selectModeNotifier = - ValueNotifier(SelectMode.normal); + bool get displayNavigationBar => false; - final ValueNotifier> - conversationSelectionNotifier = ValueNotifier([]); + Stream get clientStream => _clientStream.stream; - bool isSearching = false; - static const String _serverStoreNamespace = 'im.fluffychat.search.server'; + // Needs to match GroupsSpacesEntry for 'separate group' checking. + List get spaces => client.rooms.where((r) => r.isSpace).toList(); - final TextEditingController searchChatController = TextEditingController(); + String? get activeRoomId => widget.activeRoomId; - void _search() async { - final client = Matrix.of(context).client; - if (!isSearching) { - setState(() { - isSearching = true; - }); - } - SearchUserDirectoryResponse? userSearchResult; - QueryPublicRoomsResponse? roomSearchResult; - try { - roomSearchResult = await client.queryPublicRooms( - server: searchServer, - filter: PublicRoomQueryFilter( - genericSearchTerm: searchChatController.text, - ), - limit: 20, - ); - userSearchResult = await client.searchUserDirectory( - searchChatController.text, - limit: 20, + bool get isSelectMode => selectModeNotifier.value == SelectMode.select; + + bool get anySelectedRoomNotMarkedUnread => + conversationSelectionNotifier.value.any( + (conversation) => !Matrix.of(context) + .client + .getRoomById(conversation.roomId)! + .isUnreadOrInvited, ); - } catch (e, s) { - Logs().w('Searching has crashed', e, s); - TwakeSnackBar.show(context, e.toLocalizedString(context)); - } - if (!isSearchMode) return; - setState(() { - isSearching = false; - this.roomSearchResult = roomSearchResult; - this.userSearchResult = userSearchResult; - }); - } - void onSearchEnter(String text) { - if (text.isEmpty) { - cancelSearch(unfocus: false); - return; - } + bool get anySelectedRoomNotFavorite => + conversationSelectionNotifier.value.any( + (conversation) => !Matrix.of(context) + .client + .getRoomById(conversation.roomId)! + .isFavourite, + ); - setState(() { - isSearchMode = true; - }); - _coolDown?.cancel(); - _coolDown = Timer(const Duration(milliseconds: 500), _search); - } + bool get anySelectedRoomNotMuted => conversationSelectionNotifier.value.any( + (conversation) => + Matrix.of(context) + .client + .getRoomById(conversation.roomId)! + .pushRuleState == + PushRuleState.notify, + ); - void cancelSearch({bool unfocus = true}) { - setState(() { - searchChatController.clear(); - isSearchMode = false; - roomSearchResult = userSearchResult = null; - isSearching = false; - }); - if (unfocus) FocusManager.instance.primaryFocus?.unfocus(); - } + bool get displayBundles => + Matrix.of(context).hasComplexBundles && + Matrix.of(context).accountBundles.keys.length > 1; - bool isTorBrowser = false; + bool get filteredRoomsForAllIsEmpty => filteredRoomsForAll.isEmpty; - BoxConstraints? snappingSheetContainerSize; + bool get filteredRoomsForPinIsEmpty => filteredRoomsForPin.isEmpty; - final ScrollController scrollController = ScrollController(); - bool scrolledToTop = true; + bool get chatListBodyIsEmpty => + filteredRoomsForAllIsEmpty && filteredRoomsForPinIsEmpty; - final StreamController _clientStream = StreamController.broadcast(); + bool get conversationSelectionNotifierIsEmpty => + conversationSelectionNotifier.value.isEmpty; - Stream get clientStream => _clientStream.stream; + PushRuleState get pushRuleState => anySelectedRoomNotMuted + ? PushRuleState.mentionsOnly + : PushRuleState.notify; void addAccountAction() => context.go('/settings/account'); @@ -192,49 +176,21 @@ class ChatListController extends State } void editSpace(BuildContext context, String spaceId) async { - await Matrix.of(context).client.getRoomById(spaceId)!.postLoad(); + await client.getRoomById(spaceId)!.postLoad(); if (mounted) { context.go('/spaces/$spaceId'); } } - // Needs to match GroupsSpacesEntry for 'separate group' checking. - List get spaces => - Matrix.of(context).client.rooms.where((r) => r.isSpace).toList(); - - String? get activeRoomId => widget.activeRoomId; - - bool get isSelectMode => selectModeNotifier.value == SelectMode.select; - - @override - void initState() { - if (kIsWeb) { - BrowserContextMenu.disableContextMenu(); + String? get secureActiveBundle { + if (Matrix.of(context).activeBundle == null || + !Matrix.of(context) + .accountBundles + .keys + .contains(Matrix.of(context).activeBundle)) { + return Matrix.of(context).accountBundles.keys.first; } - initReceiveSharingIntent(); - - scrollController.addListener(_onScroll); - _waitForFirstSync(); - _hackyWebRTCFixForWeb(); - CallKeepManager().initialize(); - WidgetsBinding.instance.addPostFrameCallback((_) async { - if (mounted) { - searchServer = await Store().getItem(_serverStoreNamespace); - Matrix.of(context).backgroundPush?.setupPush(); - } - }); - - _checkTorBrowser(); - super.initState(); - } - - @override - void dispose() { - intentDataStreamSubscription?.cancel(); - intentFileStreamSubscription?.cancel(); - intentUriStreamSubscription?.cancel(); - scrollController.removeListener(_onScroll); - super.dispose(); + return Matrix.of(context).activeBundle; } void toggleSelection(String roomId) { @@ -264,7 +220,11 @@ class ChatListController extends State void toggleSelectMode() { selectModeNotifier.value = isSelectMode ? SelectMode.normal : SelectMode.select; - _clearSelectionItem(); + } + + Future actionWithToggleSelectMode(Function action) async { + await action(); + toggleSelectMode(); } void _clearSelectionItem() { @@ -279,6 +239,18 @@ class ChatListController extends State } } + void resetActiveSpaceId() { + setState(() { + activeSpaceId = null; + }); + } + + void setActiveSpace(String? spaceId) { + setState(() { + activeSpaceId = spaceId; + }); + } + Future toggleUnread() async { await showFutureLoadingDialog( context: context, @@ -291,7 +263,6 @@ class ChatListController extends State } }, ); - toggleSelectMode(); } Future toggleFavouriteRoom() async { @@ -308,26 +279,21 @@ class ChatListController extends State } }, ); - toggleSelectMode(); } Future toggleMuted() async { await showFutureLoadingDialog( context: context, future: () async { - final newState = anySelectedRoomNotMuted - ? PushRuleState.mentionsOnly - : PushRuleState.notify; for (final conversation in conversationSelectionNotifier.value) { final room = client.getRoomById(conversation.roomId)!; - if (room.pushRuleState == newState) continue; + if (room.pushRuleState == pushRuleState) continue; await client .getRoomById(conversation.roomId)! - .setPushRuleState(newState); + .setPushRuleState(pushRuleState); } }, ); - toggleSelectMode(); } Future archiveAction() async { @@ -363,11 +329,11 @@ class ChatListController extends State if (input == null) return; await showFutureLoadingDialog( context: context, - future: () => Matrix.of(context).client.setPresence( - Matrix.of(context).client.userID!, - PresenceType.online, - statusMsg: input.single, - ), + future: () => client.setPresence( + client.userID!, + PresenceType.online, + statusMsg: input.single, + ), ); } @@ -405,7 +371,7 @@ class ChatListController extends State final result = await showFutureLoadingDialog( context: context, future: () async { - final space = Matrix.of(context).client.getRoomById(selectedSpace)!; + final space = client.getRoomById(selectedSpace)!; if (space.canSendDefaultStates) { for (final conversation in conversationSelectionNotifier.value) { await space.setSpaceChild(conversation.roomId); @@ -424,33 +390,6 @@ class ChatListController extends State conversationSelectionNotifier.value.clear(); } - bool get anySelectedRoomNotMarkedUnread => - conversationSelectionNotifier.value.any( - (conversation) => !Matrix.of(context) - .client - .getRoomById(conversation.roomId)! - .markedUnread, - ); - - bool get anySelectedRoomNotFavorite => - conversationSelectionNotifier.value.any( - (conversation) => !Matrix.of(context) - .client - .getRoomById(conversation.roomId)! - .isFavourite, - ); - - bool get anySelectedRoomNotMuted => conversationSelectionNotifier.value.any( - (conversation) => - Matrix.of(context) - .client - .getRoomById(conversation.roomId)! - .pushRuleState == - PushRuleState.notify, - ); - - bool waitForFirstSync = false; - Future _waitForFirstSync() async { await client.roomsLoading; await client.accountDataLoading; @@ -516,7 +455,7 @@ class ChatListController extends State Matrix.of(context).activeBundle = bundle; if (!Matrix.of(context) .currentBundle! - .any((client) => client == Matrix.of(context).client)) { + .any((client) => client == client)) { Matrix.of(context) .setActiveClient(Matrix.of(context).currentBundle!.first); } @@ -565,39 +504,25 @@ class ChatListController extends State } } - bool get displayBundles => - Matrix.of(context).hasComplexBundles && - Matrix.of(context).accountBundles.keys.length > 1; - - String? get secureActiveBundle { - if (Matrix.of(context).activeBundle == null || - !Matrix.of(context) - .accountBundles - .keys - .contains(Matrix.of(context).activeBundle)) { - return Matrix.of(context).accountBundles.keys.first; - } - return Matrix.of(context).activeBundle; - } - void _onTapBottomNavigation( ChatListSelectionActions chatListBottomNavigatorBar, ) async { switch (chatListBottomNavigatorBar) { case ChatListSelectionActions.read: - await toggleUnread(); - toggleSelectMode(); + await actionWithToggleSelectMode(toggleUnread); return; case ChatListSelectionActions.mute: - await toggleMuted(); - toggleSelectMode(); + await actionWithToggleSelectMode(toggleMuted); return; case ChatListSelectionActions.pin: - await toggleFavouriteRoom(); - toggleSelectMode(); + await actionWithToggleSelectMode(toggleFavouriteRoom); return; case ChatListSelectionActions.more: - toggleSelectMode(); + await actionWithToggleSelectMode( + () => { + TwakeSnackBar.show(context, 'Not implemented yet'), + }, + ); return; } } @@ -618,8 +543,10 @@ class ChatListController extends State Room room, ) { final listAction = [ - ChatListSelectionActions.read, - ChatListSelectionActions.pin, + if (room.membership != Membership.invite) ...[ + ChatListSelectionActions.read, + ChatListSelectionActions.pin, + ], ChatListSelectionActions.mute, ]; return listAction.map((action) { @@ -663,7 +590,11 @@ class ChatListController extends State await showFutureLoadingDialog( context: context, future: () async { - await room.mute(); + await client.getRoomById(room.id)!.setPushRuleState( + room.pushRuleState == PushRuleState.notify + ? PushRuleState.mentionsOnly + : PushRuleState.notify, + ); }, ); return; @@ -741,16 +672,6 @@ class ChatListController extends State } } - @override - Widget build(BuildContext context) { - return ChatListView( - controller: this, - bottomNavigationBar: widget.bottomNavigationBar, - onOpenSearchPage: widget.onOpenSearchPage, - onTapBottomNavigation: _onTapBottomNavigation, - ); - } - void _hackyWebRTCFixForWeb() { ChatList.contextForVoip = context; } @@ -772,6 +693,60 @@ class ChatListController extends State Future dehydrate() => SettingsSecurityController.dehydrateDevice(context); + + void onLongPressChatListItem(Room room) { + if (!isSelectMode) { + toggleSelectMode(); + _handleOnLongPressInSelectMode(room); + } + } + + void _handleOnLongPressInSelectMode(Room room) { + if (conversationSelectionNotifierIsEmpty) { + toggleSelection( + room.id, + ); + } + } + + @override + void initState() { + if (kIsWeb) { + BrowserContextMenu.disableContextMenu(); + } + scrollController.addListener(_onScroll); + _waitForFirstSync(); + _hackyWebRTCFixForWeb(); + CallKeepManager().initialize(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (mounted) { + Matrix.of(context).backgroundPush?.setupPush(); + } + }); + + _checkTorBrowser(); + super.initState(); + } + + @override + void dispose() { + scrollController.removeListener(_onScroll); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return ChatListView( + controller: this, + bottomNavigationBar: widget.bottomNavigationBar, + onOpenSearchPage: widget.onOpenSearchPage, + onTapBottomNavigation: _onTapBottomNavigation, + ); + } + + @override + bool get wantKeepAlive => true; } enum EditBundleAction { addToBundle, removeFromBundle } diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart deleted file mode 100644 index c19a5dec38..0000000000 --- a/lib/pages/chat_list/chat_list_body.dart +++ /dev/null @@ -1,282 +0,0 @@ -import 'package:animations/animations.dart'; -import 'package:collection/collection.dart'; -import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; -import 'package:fluffychat/pages/chat_list/search_title.dart'; -import 'package:fluffychat/pages/chat_list/space_view.dart'; -import 'package:fluffychat/pages/chat_list/stories_header.dart'; -import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; -import 'package:fluffychat/resource/image_paths.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/utils/stream_extension.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:matrix/matrix.dart'; - -import '../../config/themes.dart'; -import '../../widgets/connection_status_header.dart'; -import '../../widgets/matrix.dart'; - -class ChatListViewBody extends StatelessWidget { - final ChatListController controller; - - const ChatListViewBody(this.controller, {Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final client = Matrix.of(context).client; - - return PageTransitionSwitcher( - transitionBuilder: ( - Widget child, - Animation primaryAnimation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - animation: primaryAnimation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.vertical, - fillColor: Theme.of(context).scaffoldBackgroundColor, - child: child, - ); - }, - child: StreamBuilder( - key: ValueKey( - client.userID.toString() + - controller.activeFilter.toString() + - controller.activeSpaceId.toString(), - ), - stream: client.onSync.stream - .where((s) => s.hasRoomUpdate) - .rateLimit(const Duration(seconds: 1)), - builder: (context, _) { - if (controller.activeFilter == ActiveFilter.spaces && - !controller.isSearchMode) { - return SpaceView( - controller, - scrollController: controller.scrollController, - key: Key(controller.activeSpaceId ?? 'Spaces'), - ); - } - if (controller.waitForFirstSync && client.prevBatch != null) { - final rooms = controller.filteredRoomsForAll; - const displayStoriesHeader = false; - if (rooms.isEmpty && !controller.isSearchMode) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 64), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SvgPicture.asset( - ImagePaths.icSkeletons, - ), - ], - ), - const SizedBox(height: 16), - FutureBuilder( - // ignore: unnecessary_cast - future: client.fetchOwnProfile(getFromRooms: false), - builder: (context, snapshotProfile) { - if (snapshotProfile.connectionState != - ConnectionState.done) { - return const SizedBox(); - } - final name = snapshotProfile.data?.displayName ?? '👋'; - return Column( - children: [ - Text( - L10n.of(context)!.welcomeToTwake(name), - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - ), - Padding( - padding: const EdgeInsets.only( - left: 32, - right: 32, - top: 8, - ), - child: Text( - L10n.of(context)!.startNewChatMessage, - style: Theme.of(context).textTheme.bodyMedium, - textAlign: TextAlign.center, - ), - ), - ], - ); - }, - ), - ], - ); - } - return ListView.builder( - controller: controller.scrollController, - // add +1 space below in order to properly scroll below the spaces bar - itemCount: rooms.length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == 0) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - //FIXME: https://github.com/linagora/twake-on-matrix/issues/465 - // if (controller.isSearchMode) ...[ - // ..._buildPublicRooms(context, roomSearchResult), - // ..._buildUsers(context, userSearchResult), - // SearchTitle( - // title: L10n.of(context)!.stories, - // icon: const Icon(Icons.camera_alt_outlined), - // ), - // ], - if (displayStoriesHeader) - // ignore: dead_code - StoriesHeader( - key: const Key('stories_header'), - filter: controller.searchChatController.text, - ), - const ConnectionStatusHeader(), - AnimatedContainer( - height: controller.isTorBrowser ? 64 : 0, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - child: Material( - color: Theme.of(context).colorScheme.surface, - child: ListTile( - leading: const Icon(Icons.vpn_key), - title: Text(L10n.of(context)!.dehydrateTor), - subtitle: Text(L10n.of(context)!.dehydrateTorLong), - trailing: const Icon(Icons.chevron_right_outlined), - onTap: controller.dehydrate, - ), - ), - ), - if (controller.isSearchMode) - SearchTitle( - title: L10n.of(context)!.chats, - icon: const Icon(Icons.chat_outlined), - ), - ], - ); - } - index--; - if (!rooms[index] - .getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)) - .toLowerCase() - .contains( - controller.searchChatController.text.toLowerCase(), - )) { - return Container(); - } - return ValueListenableBuilder( - valueListenable: controller.selectModeNotifier, - builder: (context, selectedRoomIds, _) { - return ChatListItem( - rooms[index], - key: Key('chat_list_item_${rooms[index].id}'), - isEnableSelectMode: controller.isSelectMode, - onTap: controller.isSelectMode - ? () => controller.toggleSelection(rooms[index].id) - : null, - onSecondaryTap: () => controller.handleContextMenuAction( - context, - rooms[index], - ), - onLongPress: () { - controller.toggleSelectMode(); - controller.toggleSelection( - rooms[index].id, - ); - }, - checkBoxWidget: ValueListenableBuilder( - valueListenable: - controller.conversationSelectionNotifier, - builder: (context, conversationSelection, __) { - final conversation = - conversationSelection.firstWhereOrNull( - (conversation) => - conversation.roomId.contains(rooms[index].id), - ); - return Checkbox( - value: conversation?.isSelected == true, - onChanged: (_) { - controller.toggleSelection(rooms[index].id); - }, - ); - }, - ), - activeChat: controller.activeRoomId == rooms[index].id, - ); - }, - ); - }, - ); - } - const dummyChatCount = 5; - final titleColor = - Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100); - final subtitleColor = - Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(50); - return ListView.builder( - key: const Key('dummychats'), - itemCount: dummyChatCount, - itemBuilder: (context, i) => Opacity( - opacity: (dummyChatCount - i) / dummyChatCount, - child: ListTile( - leading: CircleAvatar( - backgroundColor: titleColor, - child: CircularProgressIndicator( - strokeWidth: 1, - color: Theme.of(context).textTheme.bodyLarge!.color, - ), - ), - title: Row( - children: [ - Expanded( - child: Container( - height: 14, - decoration: BoxDecoration( - color: titleColor, - borderRadius: BorderRadius.circular(3), - ), - ), - ), - const SizedBox(width: 36), - Container( - height: 14, - width: 14, - decoration: BoxDecoration( - color: subtitleColor, - borderRadius: BorderRadius.circular(14), - ), - ), - const SizedBox(width: 12), - Container( - height: 14, - width: 14, - decoration: BoxDecoration( - color: subtitleColor, - borderRadius: BorderRadius.circular(14), - ), - ), - ], - ), - subtitle: Container( - decoration: BoxDecoration( - color: subtitleColor, - borderRadius: BorderRadius.circular(3), - ), - height: 12, - margin: const EdgeInsets.only(right: 22), - ), - ), - ), - ); - }, - ), - ); - } -} diff --git a/lib/pages/chat_list/chat_list_body_stream.dart b/lib/pages/chat_list/chat_list_body_stream.dart deleted file mode 100644 index 51f9e06777..0000000000 --- a/lib/pages/chat_list/chat_list_body_stream.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; -import 'package:fluffychat/config/themes.dart'; - -import 'chat_list_body.dart'; - -class ChatListBodyStream extends StatelessWidget { - final ChatListController controller; - - const ChatListBodyStream({ - super.key, - required this.controller, - }); - - @override - Widget build(BuildContext context) { - return StreamBuilder( - stream: Matrix.of(context).onShareContentChanged.stream, - builder: (_, __) { - return Row( - children: [ - if (FluffyThemes.isColumnMode(context) && - FluffyThemes.getDisplayNavigationRail(context)) ...[ - Container( - color: Theme.of(context).dividerColor, - width: 1, - ), - ], - Expanded( - child: GestureDetector( - onTap: FocusManager.instance.primaryFocus?.unfocus, - excludeFromSemantics: true, - behavior: HitTestBehavior.translucent, - child: ChatListViewBody(controller), - ), - ), - ], - ); - }, - ); - } -} diff --git a/lib/pages/chat_list/chat_list_body_view.dart b/lib/pages/chat_list/chat_list_body_view.dart new file mode 100644 index 0000000000..5e76da71ae --- /dev/null +++ b/lib/pages/chat_list/chat_list_body_view.dart @@ -0,0 +1,268 @@ +import 'package:animations/animations.dart'; +import 'package:fluffychat/config/themes.dart'; +import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pages/chat_list/chat_list_body_view_style.dart'; +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'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; +import 'package:matrix/matrix.dart'; + +class ChatListBodyView extends StatelessWidget { + final ChatListController controller; + + const ChatListBodyView(this.controller, {Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (FluffyThemes.isColumnMode(context) && + FluffyThemes.getDisplayNavigationRail(context)) ...[ + Container( + color: Theme.of(context).dividerColor, + width: 1, + ), + ], + Expanded( + child: GestureDetector( + onTap: FocusManager.instance.primaryFocus?.unfocus, + excludeFromSemantics: true, + behavior: HitTestBehavior.translucent, + child: PageTransitionSwitcher( + transitionBuilder: ( + Widget child, + Animation primaryAnimation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + animation: primaryAnimation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.vertical, + fillColor: Theme.of(context).scaffoldBackgroundColor, + child: child, + ); + }, + child: StreamBuilder( + key: ValueKey( + controller.client.userID.toString() + + controller.activeFilter.toString() + + controller.activeSpaceId.toString(), + ), + stream: controller.client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, _) { + if (controller.activeFilter == ActiveFilter.spaces) { + return SpaceView( + controller, + scrollController: controller.scrollController, + key: Key(controller.activeSpaceId ?? 'Spaces'), + ); + } + if (controller.waitForFirstSync && + controller.client.prevBatch != null) { + if (controller.chatListBodyIsEmpty) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: ChatListBodyViewStyle.paddingIconSkeletons, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SvgPicture.asset( + ImagePaths.icSkeletons, + ), + ], + ), + ), + Padding( + padding: ChatListBodyViewStyle.paddingOwnProfile, + child: FutureBuilder( + future: controller.client + .fetchOwnProfile(getFromRooms: false), + builder: (context, snapshotProfile) { + if (snapshotProfile.connectionState != + ConnectionState.done) { + return const SizedBox(); + } + final name = + snapshotProfile.data?.displayName ?? '👋'; + return Column( + children: [ + Text( + L10n.of(context)!.welcomeToTwake(name), + style: Theme.of(context) + .textTheme + .titleLarge, + textAlign: TextAlign.center, + ), + Padding( + padding: ChatListBodyViewStyle + .paddingTextStartNewChatMessage, + child: Text( + L10n.of(context)!.startNewChatMessage, + style: Theme.of(context) + .textTheme + .bodyMedium, + textAlign: TextAlign.center, + ), + ), + ], + ); + }, + ), + ), + ], + ); + } + return SingleChildScrollView( + controller: controller.scrollController, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ConnectionStatusHeader(), + AnimatedContainer( + height: ChatListBodyViewStyle.heightIsTorBrowser( + controller.isTorBrowser, + ), + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + child: Material( + color: Theme.of(context).colorScheme.surface, + child: ListTile( + leading: const Icon(Icons.vpn_key), + title: Text(L10n.of(context)!.dehydrateTor), + subtitle: + Text(L10n.of(context)!.dehydrateTorLong), + trailing: + const Icon(Icons.chevron_right_outlined), + onTap: controller.dehydrate, + ), + ), + ), + if (!controller.filteredRoomsForPinIsEmpty) + ValueListenableBuilder( + valueListenable: + controller.expandRoomsForPinNotifier, + builder: (context, isExpanded, child) { + return Column( + 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: ChatListViewBuilder( + controller: controller, + rooms: controller.filteredRoomsForPin, + ), + ), + 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, + ), + ), + ], + ), + ); + } + return const SizedBox.shrink(); + }, + ), + ), + ), + ), + ], + ); + } +} + +class ExpandableTitleBuilder extends StatelessWidget { + final String title; + final bool isExpanded; + final VoidCallback? onTap; + + const ExpandableTitleBuilder({ + super.key, + required this.title, + this.isExpanded = false, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: ChatListBodyViewStyle.paddingHorizontalExpandableTitleBuilder, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: LinagoraRefColors.material().neutral[40], + ), + ), + InkWell( + onTap: onTap, + child: Padding( + padding: ChatListBodyViewStyle.paddingIconExpand, + child: Icon( + isExpanded ? Icons.expand_less : Icons.expand_more, + size: ChatListBodyViewStyle.sizeIconExpand, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat_list/chat_list_body_view_style.dart b/lib/pages/chat_list/chat_list_body_view_style.dart new file mode 100644 index 0000000000..52ec33e1b0 --- /dev/null +++ b/lib/pages/chat_list/chat_list_body_view_style.dart @@ -0,0 +1,37 @@ +import 'package:flutter/cupertino.dart'; + +class ChatListBodyViewStyle { + static const double sizeIconExpand = 24; + + static double heightIsTorBrowser(bool isTorBrowser) => isTorBrowser ? 64 : 0; + + static const EdgeInsetsDirectional paddingIconSkeletons = + EdgeInsetsDirectional.only( + top: 64, + ); + + static const EdgeInsetsDirectional paddingOwnProfile = + EdgeInsetsDirectional.only( + top: 16, + ); + + static const EdgeInsetsDirectional paddingTextStartNewChatMessage = + EdgeInsetsDirectional.only( + start: 32, + end: 32, + top: 8, + ); + + static const EdgeInsetsDirectional paddingTopExpandableTitleBuilder = + EdgeInsetsDirectional.only( + top: 8, + ); + + static const EdgeInsetsDirectional paddingHorizontalExpandableTitleBuilder = + EdgeInsetsDirectional.symmetric( + horizontal: 16, + ); + + static const EdgeInsetsDirectional paddingIconExpand = + EdgeInsetsDirectional.all(8); +} diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 0e67a69a8f..6c75cba2a0 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -1,6 +1,6 @@ import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:fluffychat/config/app_config.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_item_mixin.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'; import 'package:fluffychat/pages/chat_list/chat_list_item_title.dart'; @@ -11,6 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; import 'package:go_router/go_router.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; enum ArchivedRoomAction { delete, rejoin } @@ -84,9 +85,11 @@ class ChatListItem extends StatelessWidget with ChatListItemMixin { } } + bool get _isGroupChat => !room.isDirectChat; + @override Widget build(BuildContext context) { - final displayname = room.getLocalizedDisplayname( + final displayName = room.getLocalizedDisplayname( MatrixLocals(L10n.of(context)!), ); return Padding( @@ -97,7 +100,7 @@ class ChatListItem extends StatelessWidget with ChatListItemMixin { color: isSelectedItem ? Theme.of(context).colorScheme.primaryContainer : activeChat - ? Theme.of(context).colorScheme.surface + ? Theme.of(context).colorScheme.secondaryContainer : Colors.transparent, child: InkWell( onTap: () => clickAction(context), @@ -110,10 +113,34 @@ class ChatListItem extends StatelessWidget with ChatListItemMixin { if (isEnableSelectMode) checkBoxWidget ?? const SizedBox(), Padding( padding: ChatListItemStyle.paddingAvatar, - child: Avatar( - mxContent: room.avatar, - name: displayname, - onTap: onTapAvatar, + 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( diff --git a/lib/pages/chat_list/chat_list_item_style.dart b/lib/pages/chat_list/chat_list_item_style.dart index f90a1fb248..effb7f8d86 100644 --- a/lib/pages/chat_list/chat_list_item_style.dart +++ b/lib/pages/chat_list/chat_list_item_style.dart @@ -3,9 +3,13 @@ import 'package:linagora_design_flutter/colors/linagora_ref_colors.dart'; class ChatListItemStyle { static Color? get readIconColor => LinagoraRefColors.material().tertiary[20]; - static double get readIconSize => 20; - static double get mentionIconWidth => 20; - static Color get readMessageColor => const Color(0xFF787579); + + static const double readIconSize = 20; + + static const double groupIconSize = 16; + + static const double mentionIconWidth = 20; + static double unreadBadgeSize( bool unread, bool hasNewMessages, @@ -21,16 +25,19 @@ class ChatListItemStyle { static const EdgeInsetsDirectional paddingConversation = EdgeInsetsDirectional.symmetric( horizontal: 8, - vertical: 1, + vertical: 2, ); static const EdgeInsetsDirectional paddingAvatar = EdgeInsetsDirectional.only(end: 8); - static const EdgeInsetsDirectional paddingBody = - EdgeInsetsDirectional.symmetric(horizontal: 8); + static const EdgeInsetsDirectional paddingIconGroup = + EdgeInsetsDirectional.all(4); + + static const EdgeInsetsDirectional paddingBody = EdgeInsetsDirectional.all(8); static const double unreadBadgePaddingWhenMoreThanOne = 9.0; + static double notificationBadgeSize( bool unread, bool hasNewMessages, @@ -44,5 +51,5 @@ class ChatListItemStyle { unreadBadgePaddingWhenMoreThanOne; } - static const double letterSpaceDisplayname = 0.15; + static const double letterSpaceDisplayName = 0.15; } diff --git a/lib/pages/chat_list/chat_list_item_subtitle.dart b/lib/pages/chat_list/chat_list_item_subtitle.dart index 1199955241..826341d138 100644 --- a/lib/pages/chat_list/chat_list_item_subtitle.dart +++ b/lib/pages/chat_list/chat_list_item_subtitle.dart @@ -1,6 +1,6 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_item_mixin.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'; import 'package:fluffychat/utils/room_status_extension.dart'; @@ -19,154 +19,127 @@ class ChatListItemSubtitle extends StatelessWidget with ChatListItemMixin { @override Widget build(BuildContext context) { final typingText = room.getLocalizedTypingText(context); - final unread = room.isUnread || room.membership == Membership.invite; - final ownLastMessage = - room.lastEvent?.senderId == Matrix.of(context).client.userID; final isGroup = !room.isDirectChat; final unreadBadgeSize = ChatListItemStyle.unreadBadgeSize( - unread, + room.isUnreadOrInvited, room.hasNewMessages, room.notificationCount > 0, ); - return SizedBox( - height: 39, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (typingText.isEmpty && - ownLastMessage && - room.lastEvent!.status.isSending) ...[ - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ), - const SizedBox(width: 4), - ], - Expanded( - child: typingText.isNotEmpty - ? Column( - children: [ - Expanded( - child: typingTextWidget(typingText, context), - ), - const Spacer(), - ], - ) - : (isGroup - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: lastSenderWidget(room, isGroup, unread), - ), - const SizedBox(height: 2), - Flexible( - child: textContentWidget( - room, - context, - isGroup, - unread, - ), - ) - ], + return Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: typingText.isNotEmpty + ? Column( + children: [ + Flexible( + child: typingTextWidget(typingText, context), + ), + const Spacer(), + ], + ) + : (isGroup + ? chatListItemSubtitleForGroup( + room: room, + ) + : textContentWidget( + room, + context, + isGroup, + room.isUnreadOrInvited, + )), + ), + const SizedBox(width: 8), + FutureBuilder( + future: room.lastEvent?.calcLocalizedBody( + MatrixLocals(L10n.of(context)!), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + ) ?? + Future.value(''), + builder: (context, snapshot) { + if (snapshot.data == '' || + snapshot.data == null || + room.lastEvent == null) { + return const SizedBox.shrink(); + } + + final isMentionned = snapshot.data! + .getAllMentionedUserIdsFromMessage(room) + .contains(Matrix.of(context).client.userID); + return AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.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, + ), ) - : textContentWidget(room, context, isGroup, unread)), + : Container(), + ), + ); + }, + ), + const SizedBox(width: 4), + AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + padding: const EdgeInsets.symmetric(horizontal: 7), + height: unreadBadgeSize, + width: ChatListItemStyle.notificationBadgeSize( + room.isUnreadOrInvited, + room.hasNewMessages, + room.notificationCount, ), - const SizedBox(width: 8), - FutureBuilder( - future: room.lastEvent?.calcLocalizedBody( - MatrixLocals(L10n.of(context)!), - hideReply: true, - hideEdit: true, - plaintextBody: true, - removeMarkdown: true, - ) ?? - Future.value(''), - builder: (context, snapshot) { - if (snapshot.data == '' || - snapshot.data == null || - room.lastEvent == null) { - return const SizedBox.shrink(); - } - - final isMentionned = snapshot.data! - .getAllMentionedUserIdsFromMessage(room) - .contains(Matrix.of(context).client.userID); - return AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - padding: const EdgeInsets.only(bottom: 4), - height: ChatListItemStyle.mentionIconWidth, - width: isMentionned && unread - ? ChatListItemStyle.mentionIconWidth - : 0, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - ), - child: Center( - child: isMentionned && unread - ? Text( - '@', - style: TextStyle( - color: isMentionned - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context) - .colorScheme - .onPrimaryContainer, - fontSize: Theme.of(context) - .textTheme - .labelMedium - ?.fontSize, - ), - ) - : Container(), - ), - ); - }, + decoration: BoxDecoration( + 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], + borderRadius: BorderRadius.circular(AppConfig.borderRadius), ), - const SizedBox(width: 4), - AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - padding: const EdgeInsets.symmetric(horizontal: 7), - height: unreadBadgeSize, - width: ChatListItemStyle.notificationBadgeSize( - unread, - room.hasNewMessages, - room.notificationCount, - ), - decoration: BoxDecoration( - 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], - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - ), - child: Center( - child: room.notificationCount > 0 - ? Text( - room.notificationCount.toString(), - style: Theme.of(context).textTheme.labelMedium?.copyWith( - letterSpacing: -0.5, - color: room.highlightCount > 0 - ? Theme.of(context).colorScheme.onPrimary - : room.notificationCount > 0 - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ) - : Container(), - ), + child: Center( + child: room.notificationCount > 0 + ? Text( + room.notificationCount.toString(), + style: Theme.of(context).textTheme.labelMedium?.copyWith( + letterSpacing: -0.5, + color: room.highlightCount > 0 + ? Theme.of(context).colorScheme.onPrimary + : room.notificationCount > 0 + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ) + : Container(), ), - ], - ), + ), + ], ); } } diff --git a/lib/pages/chat_list/chat_list_item_title.dart b/lib/pages/chat_list/chat_list_item_title.dart index 928d4ff573..04576a31bc 100644 --- a/lib/pages/chat_list/chat_list_item_title.dart +++ b/lib/pages/chat_list/chat_list_item_title.dart @@ -1,10 +1,13 @@ -import 'package:fluffychat/pages/chat_list/chat_list_item_mixin.dart'; +import 'package:fluffychat/pages/chat_list/chat_list_item_title_style.dart'; +import 'package:fluffychat/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_view.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/date_time_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/utils/room_status_extension.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/linagora_design_flutter.dart'; import 'package:matrix/matrix.dart'; class ChatListItemTitle extends StatelessWidget with ChatListItemMixin { @@ -14,11 +17,10 @@ class ChatListItemTitle extends StatelessWidget with ChatListItemMixin { @override Widget build(BuildContext context) { - final displayname = room.getLocalizedDisplayname( + final displayName = room.getLocalizedDisplayname( MatrixLocals(L10n.of(context)!), ); final isMuted = room.pushRuleState != PushRuleState.notify; - final unread = room.isUnread || room.membership == Membership.invite; return Row( children: [ Expanded( @@ -29,27 +31,17 @@ class ChatListItemTitle extends StatelessWidget with ChatListItemMixin { children: [ Flexible( child: Text( - displayname, + displayName, overflow: TextOverflow.ellipsis, maxLines: 1, softWrap: false, - style: Theme.of(context).textTheme.titleMedium?.merge( - TextStyle( - overflow: TextOverflow.ellipsis, - letterSpacing: - ChatListItemStyle.letterSpaceDisplayname, - color: unread - ? Theme.of(context) - .colorScheme - .onSurfaceVariant - : ChatListItemStyle.readMessageColor, - ), - ), + style: + ChatLitTitleTextStyleView.textStyle.textStyle(room), ), ), if (room.isFavourite) Padding( - padding: const EdgeInsets.only(left: 10), + padding: ChatListItemTitleStyle.paddingLeftIcon, child: Icon( Icons.push_pin_outlined, size: ChatListItemStyle.readIconSize, @@ -58,7 +50,7 @@ class ChatListItemTitle extends StatelessWidget with ChatListItemMixin { ), if (isMuted) Padding( - padding: const EdgeInsets.only(left: 10), + padding: ChatListItemTitleStyle.paddingLeftIcon, child: Icon( Icons.volume_off_outlined, size: ChatListItemStyle.readIconSize, @@ -71,17 +63,28 @@ class ChatListItemTitle extends StatelessWidget with ChatListItemMixin { ), ), Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text( - room.timeCreated.localizedTimeShort(context), - style: Theme.of(context).textTheme.labelSmall?.merge( - TextStyle( - letterSpacing: 0.5, - color: unread - ? Theme.of(context).colorScheme.onSurface - : LinagoraRefColors.material().neutral[50], - ), + padding: ChatListItemTitleStyle.paddingLeftIcon, + child: Row( + children: [ + if (room.isTypingText(context)) ...[ + Icon( + Icons.schedule, + color: LinagoraRefColors.material().neutral[50], + size: ChatListItemTitleStyle.iconScheduleSize, + ), + ], + Padding( + padding: ChatListItemTitleStyle.paddingLeftIcon, + child: Text( + room.timeCreated.localizedTimeShort(context), + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: room.isUnreadOrInvited + ? Theme.of(context).colorScheme.onSurface + : LinagoraRefColors.material().neutral[50], + ), ), + ), + ], ), ) ], diff --git a/lib/pages/chat_list/chat_list_item_title_style.dart b/lib/pages/chat_list/chat_list_item_title_style.dart new file mode 100644 index 0000000000..f6513101e2 --- /dev/null +++ b/lib/pages/chat_list/chat_list_item_title_style.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class ChatListItemTitleStyle { + static const double iconScheduleSize = 16; + + static const EdgeInsetsDirectional paddingLeftIcon = + EdgeInsetsDirectional.only( + start: 4, + ); +} diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index ab76993eb2..43ef2e08c4 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -1,5 +1,5 @@ import 'package:fluffychat/pages/chat_list/chat_list.dart'; -import 'package:fluffychat/pages/chat_list/chat_list_body_stream.dart'; +import 'package:fluffychat/pages/chat_list/chat_list_body_view.dart'; import 'package:fluffychat/pages/chat_list/chat_list_bottom_navigator.dart'; import 'package:fluffychat/pages/chat_list/chat_list_bottom_navigator_style.dart'; import 'package:fluffychat/pages/chat_list/chat_list_header.dart'; @@ -62,7 +62,7 @@ class ChatListView extends StatelessWidget { } }, ), - body: ChatListBodyStream(controller: controller), + body: ChatListBodyView(controller), floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, floatingActionButton: ValueListenableBuilder( valueListenable: controller.selectModeNotifier, diff --git a/lib/pages/chat_list/chat_list_view_builder.dart b/lib/pages/chat_list/chat_list_view_builder.dart new file mode 100644 index 0000000000..198e6557e4 --- /dev/null +++ b/lib/pages/chat_list/chat_list_view_builder.dart @@ -0,0 +1,63 @@ +import 'package:fluffychat/pages/chat_list/chat_list.dart'; +import 'package:fluffychat/pages/chat_list/chat_list_item.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +class ChatListViewBuilder extends StatelessWidget { + final ChatListController controller; + final List rooms; + + const ChatListViewBuilder({ + super.key, + required this.controller, + required this.rooms, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: rooms.length, + itemBuilder: (BuildContext context, int index) { + return ValueListenableBuilder( + valueListenable: controller.selectModeNotifier, + builder: (context, _, __) { + return ChatListItem( + rooms[index], + key: Key('chat_list_item_${rooms[index].id}'), + isEnableSelectMode: controller.isSelectMode, + onTap: controller.isSelectMode + ? () => controller.toggleSelection(rooms[index].id) + : null, + onSecondaryTap: () => controller.handleContextMenuAction( + context, + rooms[index], + ), + onLongPress: () => controller.onLongPressChatListItem( + rooms[index], + ), + checkBoxWidget: ValueListenableBuilder( + valueListenable: controller.conversationSelectionNotifier, + builder: (context, conversationSelection, __) { + final conversation = conversationSelection.firstWhereOrNull( + (conversation) => + conversation.roomId.contains(rooms[index].id), + ); + return Checkbox( + value: conversation?.isSelected == true, + onChanged: (_) { + controller.toggleSelection(rooms[index].id); + }, + ); + }, + ), + activeChat: controller.activeRoomId == rooms[index].id, + ); + }, + ); + }, + ); + } +} diff --git a/lib/pages/chat_list/receive_sharing_intent_mixin.dart b/lib/pages/chat_list/receive_sharing_intent_mixin.dart index 2b75757706..4eaa44b961 100644 --- a/lib/pages/chat_list/receive_sharing_intent_mixin.dart +++ b/lib/pages/chat_list/receive_sharing_intent_mixin.dart @@ -1,19 +1,19 @@ -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_file_extension.dart'; +import 'package:fluffychat/event/twake_event_types.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/twake_app.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'dart:async'; -import 'dart:io'; import 'package:fluffychat/config/app_config.dart'; -import 'package:go_router/go_router.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; + StreamSubscription? intentDataStreamSubscription; StreamSubscription? intentFileStreamSubscription; @@ -23,16 +23,11 @@ mixin ReceiveSharingIntentMixin on State { void _processIncomingSharedFiles(List files) { if (files.isEmpty) return; final shareFile = files.first; - final path = Uri.decodeFull(shareFile.path.replaceFirst('file://', '')); - final file = File(path); - Matrix.of(context).shareContent = { - 'msgtype': 'chat.fluffy.shared_file', - 'file': MatrixFile( - name: file.path, - filePath: file.path, - ).detectFileType, + matrixState.shareContent = { + 'msgtype': TwakeEventTypes.shareFileEventType, + 'file': shareFile.toMatrixFile(), }; - context.push('/share'); + TwakeApp.router.go('/share'); } void _processIncomingSharedText(String? text) { @@ -43,16 +38,16 @@ mixin ReceiveSharingIntentMixin on State { !RegExp(r'\s').hasMatch(text))) { return _processIncomingUris(text); } - Matrix.of(context).shareContent = { + matrixState.shareContent = { 'msgtype': 'm.text', 'body': text, }; - context.push('/share'); + TwakeApp.router.go('/share'); } void _processIncomingUris(String? text) async { if (text == null) return; - context.push('/share'); + TwakeApp.router.go('/share'); WidgetsBinding.instance.addPostFrameCallback((_) { UrlLauncher(context, text).openMatrixToUrl(); }); diff --git a/lib/pages/contacts_tab/contacts_tab_body_view.dart b/lib/pages/contacts_tab/contacts_tab_body_view.dart index 26d966e277..59a0b32cb0 100644 --- a/lib/pages/contacts_tab/contacts_tab_body_view.dart +++ b/lib/pages/contacts_tab/contacts_tab_body_view.dart @@ -20,6 +20,7 @@ class ContactsTabBodyView extends StatelessWidget { Widget build(BuildContext context) { if (controller.refreshController == null) return const SizedBox(); return TwakeSmartRefresher( + onRefresh: controller.fetchContacts, onLoading: controller.loadMoreContacts, controller: controller.refreshController!, slivers: [ diff --git a/lib/pages/forward/selectable_chat_list_item.dart b/lib/pages/forward/selectable_chat_list_item.dart deleted file mode 100644 index ad52488881..0000000000 --- a/lib/pages/forward/selectable_chat_list_item.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'package:fluffychat/pages/chat_list/chat_list_item_style.dart'; -import 'package:fluffychat/pages/forward/forward_item_style.dart'; -import 'package:fluffychat/pages/forward/selectable_chat_list_item_style.dart'; -import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; -import 'package:fluffychat/widgets/avatar/avatar.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'; - -class SelectableChatListItem extends StatelessWidget { - final Room room; - final bool selected; - final void Function()? onTap; - - const SelectableChatListItem( - this.room, { - this.selected = false, - this.onTap, - Key? key, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - final displayName = room.getLocalizedDisplayname( - MatrixLocals(L10n.of(context)!), - ); - return Material( - borderRadius: BorderRadius.circular(16.0), - clipBehavior: Clip.hardEdge, - color: Colors.transparent, - child: Theme( - data: ThemeData( - splashColor: Colors.transparent, - highlightColor: Colors.transparent, - ), - child: ListTile( - contentPadding: EdgeInsets.zero, - title: Row( - children: [ - SizedBox( - width: ForwardItemStyle.avatarSize, - child: Stack( - children: [ - Avatar( - size: ForwardItemStyle.avatarSize, - mxContent: room.avatar, - name: displayName, - ), - if (selected) - Positioned( - right: SelectableChatListItemStyle - .rightFromAvatarBottomRight, - bottom: SelectableChatListItemStyle - .bottomFromAvatarBottomRight, - child: Container( - width: ForwardItemStyle.selectedContainerSize, - height: ForwardItemStyle.selectedContainerSize, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: Icon( - Icons.check_circle, - size: ForwardItemStyle.selectedIconSize, - color: LinagoraSysColors.material().primary, - ), - ), - ), - ], - ), - ), - const SizedBox(width: 8), - Flexible( - child: Text( - displayName, - maxLines: 1, - softWrap: false, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - overflow: TextOverflow.ellipsis, - letterSpacing: - SelectableChatListItemStyle.letterSpacing, - color: room.isUnread || - room.membership == Membership.invite - ? Theme.of(context).colorScheme.onSurfaceVariant - : ChatListItemStyle.readMessageColor, - ), - ), - ), - ], - ), - onTap: onTap, - ), - ), - ); - } -} diff --git a/lib/pages/forward/selectable_chat_list_item_style.dart b/lib/pages/forward/selectable_chat_list_item_style.dart deleted file mode 100644 index 108f135b10..0000000000 --- a/lib/pages/forward/selectable_chat_list_item_style.dart +++ /dev/null @@ -1,5 +0,0 @@ -class SelectableChatListItemStyle { - static const rightFromAvatarBottomRight = -3.0; - static const bottomFromAvatarBottomRight = -3.0; - static const letterSpacing = 0.15; -} 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 a924cf1da6..7090de44ad 100644 --- a/lib/pages/new_private_chat/new_private_chat_view.dart +++ b/lib/pages/new_private_chat/new_private_chat_view.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/pages/new_private_chat/new_private_chat.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:flutter/material.dart'; @@ -24,7 +25,9 @@ class NewPrivateChatView extends StatelessWidget { ), ), body: SingleChildScrollView( - keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag, + keyboardDismissBehavior: PlatformInfos.isMobile + ? ScrollViewKeyboardDismissBehavior.manual + : ScrollViewKeyboardDismissBehavior.onDrag, padding: const EdgeInsets.only(left: 8.0, right: 10.0), controller: controller.scrollController, child: controller.contactsNotifier == null diff --git a/lib/pages/settings_dashboard/settings/settings.dart b/lib/pages/settings_dashboard/settings/settings.dart index 05759b6ba6..e8a819bf06 100644 --- a/lib/pages/settings_dashboard/settings/settings.dart +++ b/lib/pages/settings_dashboard/settings/settings.dart @@ -8,6 +8,7 @@ import 'package:fluffychat/pages/bootstrap/bootstrap_dialog.dart'; import 'package:fluffychat/pages/connect/connect_page_mixin.dart'; import 'package:fluffychat/presentation/enum/settings/settings_enum.dart'; import 'package:fluffychat/presentation/extensions/client_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/url_launcher.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; @@ -46,6 +47,7 @@ class SettingsController extends State with ConnectPageMixin { SettingEnum.appLanguage, SettingEnum.devices, SettingEnum.help, + SettingEnum.about, SettingEnum.logout, ]; @@ -142,34 +144,50 @@ class SettingsController extends State with ConnectPageMixin { void goToSettingsProfile() async { optionsSelectNotifier.value = SettingEnum.profile; - context.go('/rooms/profile'); + final result = await context.push('/rooms/profile'); + if (result == null) { + optionsSelectNotifier.value = null; + } } - void onClickToSettingsItem(SettingEnum settingEnum) { + void onClickToSettingsItem(SettingEnum settingEnum) async { optionsSelectNotifier.value = settingEnum; switch (settingEnum) { case SettingEnum.chatSettings: - context.go('/rooms/chat'); + final result = await context.push('/rooms/chat'); + if (result == null) { + optionsSelectNotifier.value = null; + } break; case SettingEnum.privacyAndSecurity: - context.go('/rooms/security'); + final result = await context.push('/rooms/security'); + if (result == null) { + optionsSelectNotifier.value = null; + } break; case SettingEnum.notificationAndSounds: - context.go('/rooms/notifications'); + final result = await context.push('/rooms/notifications'); + if (result == null) { + optionsSelectNotifier.value = null; + } break; case SettingEnum.chatFolders: break; case SettingEnum.appLanguage: break; case SettingEnum.devices: - context.go('/rooms/devices'); - break; + final result = await context.push('/rooms/devices'); + if (result == null) { + optionsSelectNotifier.value = null; + } case SettingEnum.help: UrlLauncher( context, AppConfig.supportUrl, ).openUrlInAppBrowser(); break; + case SettingEnum.about: + PlatformInfos.showDialog(context); case SettingEnum.logout: logoutAction(); break; @@ -205,8 +223,13 @@ class SettingsController extends State with ConnectPageMixin { @override void dispose() { onAccountDataSubscription?.cancel(); - avatarUriNotifier.dispose(); - displayNameNotifier.dispose(); + if (avatarUriNotifier.value != null) { + avatarUriNotifier.dispose(); + } + + if (displayNameNotifier.value != null) { + displayNameNotifier.dispose(); + } super.dispose(); } diff --git a/lib/pages/settings_dashboard/settings_chat/settings_chat_view.dart b/lib/pages/settings_dashboard/settings_chat/settings_chat_view.dart index e50b1242aa..5836f0beda 100644 --- a/lib/pages/settings_dashboard/settings_chat/settings_chat_view.dart +++ b/lib/pages/settings_dashboard/settings_chat/settings_chat_view.dart @@ -65,12 +65,6 @@ class SettingsChatView extends StatelessWidget { defaultValue: AppConfig.autoplayImages, ), const Divider(), - SettingsSwitchListTile.adaptive( - title: L10n.of(context)!.sendOnEnter, - onChanged: (b) => AppConfig.sendOnEnter = b, - storeKey: SettingKeys.sendOnEnter, - defaultValue: AppConfig.sendOnEnter, - ), if (Matrix.of(context).webrtcIsSupported) SettingsSwitchListTile.adaptive( title: L10n.of(context)!.experimentalVideoCalls, diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart index 8ed48faee1..60ff74ec2b 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/domain/usecase/room/upload_content_interactor.dart'; import 'package:fluffychat/domain/usecase/settings/update_profile_interactor.dart'; import 'package:fluffychat/event/twake_event_dispatcher.dart'; import 'package:fluffychat/event/twake_inapp_event_types.dart'; +import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_context_menu_actions.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_avatar_ui_state.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view.dart'; @@ -23,6 +24,8 @@ import 'package:fluffychat/utils/extension/value_notifier_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/utils/twake_snackbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/mixins/popup_context_menu_action_mixin.dart'; +import 'package:fluffychat/widgets/mixins/popup_menu_widget_mixin.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:linagora_design_flutter/images_picker/asset_counter.dart'; @@ -41,12 +44,18 @@ class SettingsProfile extends StatefulWidget { } class SettingsProfileController extends State - with CommonMediaPickerMixin, SingleImagePickerMixin { + with + CommonMediaPickerMixin, + SingleImagePickerMixin, + PopupContextMenuActionMixin, + PopupMenuWidgetMixin { final uploadProfileInteractor = getIt.get(); final uploadContentInteractor = getIt.get(); final uploadContentWebInteractor = getIt.get(); + final MenuController menuController = MenuController(); + Profile? currentProfile; AssetEntity? assetEntity; FilePickerResult? filePickerResult; @@ -94,9 +103,7 @@ class SettingsProfileController extends State label: L10n.of(context)!.changeProfilePhoto, icon: Icons.add_a_photo_outlined, ), - if (currentProfile?.avatarUrl != null || - assetEntity != null || - filePickerResult != null) + if (currentProfile?.avatarUrl != null) SheetAction( key: AvatarAction.remove, label: L10n.of(context)!.removeYourAvatar, @@ -128,21 +135,27 @@ class SettingsProfileController extends State } void _handleRemoveAvatarAction() async { - if ((assetEntity != null || filePickerResult != null) && - currentProfile?.avatarUrl == null) { + if (assetEntity != null || filePickerResult != null) { + isEditedProfileNotifier.toggle(); + } + if (currentProfile?.avatarUrl == null) { _clearImageInLocal(); - return; + settingsProfileUIState.value = Right( + GetProfileUIStateSuccess( + currentProfile!, + ), + ); + } else { + TwakeLoadingDialog.showLoadingDialog(context); + final newProfile = Profile( + userId: client.userID!, + displayName: displayName, + avatarUrl: null, + ); + settingsProfileUIState.value = + Right(GetProfileUIStateSuccess(newProfile)); + _uploadProfile(isDeleteAvatar: true); } - TwakeLoadingDialog.showLoadingDialog(context); - final newProfile = Profile( - userId: client.userID!, - displayName: displayNameEditingController.text, - avatarUrl: null, - ); - settingsProfileUIState.value = - Right(GetProfileUIStateSuccess(newProfile)); - _uploadProfile(isDeleteAvatar: true); - return; } void _getImageOnWeb( @@ -215,14 +228,12 @@ class SettingsProfileController extends State return imagePickerController; } - void setAvatarAction() async { - final action = actions().isEmpty - ? actions().single.key - : await showModalActionSheet( - context: context, - title: L10n.of(context)!.changeYourAvatar, - actions: actions(), - ); + void onTapAvatarInMobile() async { + final action = await showModalActionSheet( + context: context, + title: L10n.of(context)!.changeYourAvatar, + actions: actions(), + ); if (action == null) return; if (action == AvatarAction.remove) { _handleRemoveAvatarAction(); @@ -231,6 +242,41 @@ class SettingsProfileController extends State _showImagesPickerAction(); } + List listContextMenuBuilder( + BuildContext context, + ) { + final listAction = [ + SettingsProfileContextMenuActions.edit, + SettingsProfileContextMenuActions.delete, + ]; + return listAction.map((action) { + return PopupMenuItem( + padding: EdgeInsets.zero, + child: popupItem( + context, + action.getTitle(context), + iconAction: action.getIcon(), + isClearCurrentPage: false, + onCallbackAction: () { + menuController.close(); + _handleActionContextMenu(action); + }, + ), + ); + }).toList(); + } + + void _handleActionContextMenu(SettingsProfileContextMenuActions action) { + switch (action) { + case SettingsProfileContextMenuActions.edit: + _showImagesPickerAction(); + break; + case SettingsProfileContextMenuActions.delete: + _handleRemoveAvatarAction(); + break; + } + } + void _sendAccountDataEvent({ required Profile profile, }) async { @@ -294,6 +340,9 @@ class SettingsProfileController extends State } void _clearImageInLocal() { + Logs().d( + 'SettingsProfile::_clearImageInLocal() - Clear image in local', + ); if (assetEntity != null) { assetEntity = null; } @@ -400,7 +449,7 @@ class SettingsProfileController extends State final newProfile = Profile( userId: client.userID!, displayName: success.displayName ?? displayName, - avatarUrl: success.avatar ?? currentProfile?.avatarUrl, + avatarUrl: success.avatar, ); _sendAccountDataEvent(profile: newProfile); if (!success.isDeleteAvatar) { diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_context_menu_actions.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_context_menu_actions.dart new file mode 100644 index 0000000000..4923527f95 --- /dev/null +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_context_menu_actions.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +enum SettingsProfileContextMenuActions { + edit, + delete; + + String getTitle(BuildContext context) { + switch (this) { + case SettingsProfileContextMenuActions.edit: + return L10n.of(context)!.changeProfilePhoto; + case SettingsProfileContextMenuActions.delete: + return L10n.of(context)!.removeYourAvatar; + } + } + + IconData getIcon() { + switch (this) { + case SettingsProfileContextMenuActions.edit: + return Icons.camera_alt_outlined; + case SettingsProfileContextMenuActions.delete: + return Icons.delete; + } + } +} diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart index 90df760cc3..f806146bbf 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view.dart @@ -83,7 +83,9 @@ class SettingsProfileView extends StatelessWidget { return SettingsProfileViewMobile( client: controller.client, settingsProfileUIState: controller.settingsProfileUIState, - onAvatarTap: () => controller.setAvatarAction(), + onTapAvatar: controller.onTapAvatarInMobile, + menuChildren: controller.listContextMenuBuilder(context), + menuController: controller.menuController, settingsProfileOptions: ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -136,6 +138,8 @@ class SettingsProfileView extends StatelessWidget { return SettingsProfileViewWeb( settingsProfileUIState: controller.settingsProfileUIState, client: controller.client, + menuChildren: controller.listContextMenuBuilder(context), + menuController: controller.menuController, basicInfoWidget: ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -213,7 +217,6 @@ class SettingsProfileView extends StatelessWidget { }, itemCount: controller.getListProfileBasicInfo.length, ), - onAvatarTap: () => controller.setAvatarAction(), ); }, ), diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart index 27fa0e98e6..c3d36bcc7b 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_mobile.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_pr import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_state/get_profile_ui_state.dart'; import 'package:fluffychat/pages/settings_dashboard/settings_profile/settings_profile_view_mobile_style.dart'; import 'package:fluffychat/presentation/extensions/client_extension.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar/avatar.dart'; import 'package:fluffychat/widgets/avatar/avatar_style.dart'; import 'package:flutter/material.dart'; @@ -15,15 +16,19 @@ import 'package:wechat_camera_picker/wechat_camera_picker.dart'; class SettingsProfileViewMobile extends StatelessWidget { final ValueNotifier> settingsProfileUIState; final Widget settingsProfileOptions; - final VoidCallback onAvatarTap; + final VoidCallback? onTapAvatar; + final List? menuChildren; + final MenuController? menuController; final Client client; const SettingsProfileViewMobile({ super.key, required this.settingsProfileOptions, - required this.onAvatarTap, + required this.onTapAvatar, required this.settingsProfileUIState, required this.client, + this.menuChildren, + this.menuController, }); @override @@ -49,7 +54,8 @@ class SettingsProfileViewMobile extends StatelessWidget { builder: (context, uiState, child) => uiState.fold( (failure) => child!, (success) { - if (success is GetAvatarInStreamUIStateSuccess) { + if (success is GetAvatarInStreamUIStateSuccess && + PlatformInfos.isMobile) { if (success.assetEntity == null) { return child!; } @@ -59,6 +65,8 @@ class SettingsProfileViewMobile extends StatelessWidget { AvatarStyle.defaultSize, ), child: AssetEntityImage( + width: AvatarStyle.defaultSize, + height: AvatarStyle.defaultSize, success.assetEntity!, thumbnailSize: const ThumbnailSize( SettingsProfileViewMobileStyle.thumbnailSize, @@ -84,6 +92,30 @@ class SettingsProfileViewMobile extends StatelessWidget { ), ); } + if (success is GetAvatarInBytesUIStateSuccess && + PlatformInfos.isWeb) { + if (success.filePickerResult == null || + success.filePickerResult?.files.single.bytes == + null) { + return child!; + } + return ClipOval( + child: SizedBox.fromSize( + size: const Size.fromRadius( + AvatarStyle.defaultSize, + ), + child: Image.memory( + success.filePickerResult!.files.single.bytes!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return const Center( + child: Icon(Icons.error_outline), + ); + }, + ), + ), + ); + } if (success is GetProfileUIStateSuccess) { final displayName = success.profile.displayName ?? client.mxid(context).localpart ?? @@ -119,27 +151,45 @@ class SettingsProfileViewMobile extends StatelessWidget { Positioned( bottom: SettingsProfileViewMobileStyle.positionedBottomSize, right: SettingsProfileViewMobileStyle.positionedRightSize, - child: InkWell( - onTap: onAvatarTap, - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular( - SettingsProfileViewMobileStyle.avatarSize, - ), - border: Border.all( - color: Theme.of(context).colorScheme.onPrimary, - width: - SettingsProfileViewMobileStyle.iconEditBorderWidth, + child: MenuAnchor( + controller: menuController, + builder: ( + BuildContext context, + MenuController menuController, + Widget? child, + ) { + return GestureDetector( + onTap: () { + if (PlatformInfos.isWeb) { + menuController.isOpen + ? menuController.close() + : menuController.open(); + } else { + onTapAvatar?.call(); + } + }, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular( + SettingsProfileViewMobileStyle.avatarSize, + ), + border: Border.all( + color: Theme.of(context).colorScheme.onPrimary, + width: SettingsProfileViewMobileStyle + .iconEditBorderWidth, + ), + ), + padding: SettingsProfileViewMobileStyle.editIconPadding, + child: Icon( + Icons.edit, + size: SettingsProfileViewMobileStyle.iconEditSize, + color: Theme.of(context).colorScheme.onPrimary, + ), ), - ), - padding: SettingsProfileViewMobileStyle.editIconPadding, - child: Icon( - Icons.edit, - size: SettingsProfileViewMobileStyle.iconEditSize, - color: Theme.of(context).colorScheme.onPrimary, - ), - ), + ); + }, + menuChildren: menuChildren ?? [], ), ), ], diff --git a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart index 2a4435ff63..9fb2cc2e74 100644 --- a/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart +++ b/lib/pages/settings_dashboard/settings_profile/settings_profile_view_web.dart @@ -16,16 +16,18 @@ class SettingsProfileViewWeb extends StatelessWidget { final ValueNotifier> settingsProfileUIState; final Widget basicInfoWidget; final Widget workIdentitiesInfoWidget; - final VoidCallback onAvatarTap; final Client client; + final List? menuChildren; + final MenuController? menuController; const SettingsProfileViewWeb({ super.key, required this.basicInfoWidget, - required this.onAvatarTap, required this.workIdentitiesInfoWidget, required this.client, required this.settingsProfileUIState, + this.menuController, + this.menuChildren, }); @override @@ -154,33 +156,48 @@ class SettingsProfileViewWeb extends StatelessWidget { .positionedBottomSize, right: SettingsProfileViewWebStyle .positionedRightSize, - child: InkWell( - onTap: onAvatarTap, - child: Container( - decoration: BoxDecoration( - color: - Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular( - SettingsProfileViewWebStyle.avatarSize, - ), - border: Border.all( - color: Theme.of(context) - .colorScheme - .onPrimary, - width: 4, + child: MenuAnchor( + controller: menuController, + builder: ( + BuildContext context, + MenuController menuController, + Widget? child, + ) { + return GestureDetector( + onTap: () => menuController.isOpen + ? menuController.close() + : menuController.open(), + child: Container( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primary, + borderRadius: BorderRadius.circular( + SettingsProfileViewWebStyle + .avatarSize, + ), + border: Border.all( + color: Theme.of(context) + .colorScheme + .onPrimary, + width: SettingsProfileViewWebStyle + .iconEditBorderWidth, + ), + ), + padding: SettingsProfileViewWebStyle + .paddingEditIcon, + child: Icon( + Icons.edit, + size: SettingsProfileViewWebStyle + .iconEditSize, + color: Theme.of(context) + .colorScheme + .onPrimary, + ), ), - ), - padding: SettingsProfileViewWebStyle - .paddingEditIcon, - child: Icon( - Icons.edit, - size: SettingsProfileViewWebStyle - .iconEditSize, - color: Theme.of(context) - .colorScheme - .onPrimary, - ), - ), + ); + }, + menuChildren: menuChildren ?? [], ), ), ], diff --git a/lib/pages/share/share.dart b/lib/pages/share/share.dart index 88fe678103..a5f176ae40 100644 --- a/lib/pages/share/share.dart +++ b/lib/pages/share/share.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; import 'package:fluffychat/domain/usecase/send_file_interactor.dart'; +import 'package:fluffychat/event/twake_event_types.dart'; import 'package:fluffychat/pages/share/share_view.dart'; import 'package:fluffychat/presentation/enum/chat_list/chat_list_enum.dart'; import 'package:fluffychat/presentation/extensions/client_extension.dart'; @@ -60,7 +61,8 @@ class ShareController extends State with SendFilesMixin { final shareContent = Matrix.of(context).shareContent; if (shareContent != null) { final shareFile = shareContent.tryGet('file'); - if (shareContent.tryGet('msgtype') == 'chat.fluffy.shared_file') { + if (shareContent.tryGet('msgtype') == + TwakeEventTypes.shareFileEventType) { context.go( '/rooms/${room.id}', extra: ChatRouterInputArgument( 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 new file mode 100644 index 0000000000..ba8b925fcd --- /dev/null +++ b/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_component.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +abstract class ChatListSubtitleTextStyleComponent { + TextStyle textStyle(Room room); +} 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 new file mode 100644 index 0000000000..1f72256782 --- /dev/null +++ b/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_decorator.dart @@ -0,0 +1,83 @@ +import 'package:fluffychat/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_component.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; +import 'package:matrix/matrix.dart'; + +abstract class ChatListSubtitleTextStyleDecorator + implements ChatListSubtitleTextStyleComponent { + final ChatListSubtitleTextStyleComponent interfaceTextStyleComponent; + + ChatListSubtitleTextStyleDecorator(this.interfaceTextStyleComponent); +} + +class ChatListSubtitleTextStyle implements ChatListSubtitleTextStyleDecorator { + final ChatListSubtitleTextStyleComponent _interfaceTextStyleComponent; + + ChatListSubtitleTextStyle(this._interfaceTextStyleComponent); + + @override + TextStyle textStyle(Room room) { + return _interfaceTextStyleComponent.textStyle(room); + } + + @override + ChatListSubtitleTextStyleComponent get interfaceTextStyleComponent => + _interfaceTextStyleComponent; +} + +class ReadChatListSubtitleTextStyleDecorator + implements ChatListSubtitleTextStyleComponent { + @override + TextStyle textStyle(Room room) { + return LinagoraTextStyle.material().bodyMedium3.copyWith( + color: LinagoraSysColors.material().onSurface, + ); + } +} + +class UnreadChatListSubtitleTextStyleDecorator + implements ChatListSubtitleTextStyleDecorator { + final ChatListSubtitleTextStyleComponent _interfaceTextStyleComponent; + + UnreadChatListSubtitleTextStyleDecorator(this._interfaceTextStyleComponent); + + @override + TextStyle textStyle(Room room) { + if (room.isUnreadOrInvited) { + return _interfaceTextStyleComponent.textStyle(room).merge( + LinagoraTextStyle.material().bodyMedium2.copyWith( + color: LinagoraSysColors.material().onSurface, + ), + ); + } else { + return _interfaceTextStyleComponent.textStyle(room); + } + } + + @override + ChatListSubtitleTextStyleComponent get interfaceTextStyleComponent => + _interfaceTextStyleComponent; +} + +class MuteChatListSubtitleTextStyleDecorator + implements ChatListSubtitleTextStyleDecorator { + final ChatListSubtitleTextStyleComponent _interfaceTextStyleComponent; + + MuteChatListSubtitleTextStyleDecorator(this._interfaceTextStyleComponent); + + @override + TextStyle textStyle(Room room) { + final isMuted = room.pushRuleState != PushRuleState.notify; + if (isMuted) { + return _interfaceTextStyleComponent.textStyle(room).copyWith( + color: LinagoraRefColors.material().tertiary[20], + ); + } else { + return _interfaceTextStyleComponent.textStyle(room); + } + } + + @override + ChatListSubtitleTextStyleComponent get interfaceTextStyleComponent => + _interfaceTextStyleComponent; +} diff --git a/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_view.dart b/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_view.dart new file mode 100644 index 0000000000..8b3b96c122 --- /dev/null +++ b/lib/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_view.dart @@ -0,0 +1,11 @@ +import 'package:fluffychat/presentation/decorators/chat_list/subtitle_text_style_decorator/subtitle_text_style_decorator.dart'; + +class ChatLitSubSubtitleTextStyleView { + static ChatListSubtitleTextStyle textStyle = ChatListSubtitleTextStyle( + MuteChatListSubtitleTextStyleDecorator( + UnreadChatListSubtitleTextStyleDecorator( + ReadChatListSubtitleTextStyleDecorator(), + ), + ), + ); +} diff --git a/lib/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_component.dart b/lib/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_component.dart new file mode 100644 index 0000000000..ffbf269049 --- /dev/null +++ b/lib/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_component.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; + +abstract class ChatListTitleTextStyleComponent { + TextStyle textStyle(Room room); +} 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 new file mode 100644 index 0000000000..a5e609acac --- /dev/null +++ b/lib/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_decorator.dart @@ -0,0 +1,83 @@ +import 'package:fluffychat/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_component.dart'; +import 'package:flutter/material.dart'; +import 'package:linagora_design_flutter/linagora_design_flutter.dart'; +import 'package:matrix/matrix.dart'; + +abstract class ChatListTitleTextStyleDecorator + implements ChatListTitleTextStyleComponent { + final ChatListTitleTextStyleComponent interfaceTextStyleComponent; + + ChatListTitleTextStyleDecorator(this.interfaceTextStyleComponent); +} + +class ChatListTitleTextStyle implements ChatListTitleTextStyleDecorator { + final ChatListTitleTextStyleComponent _interfaceTextStyleComponent; + + ChatListTitleTextStyle(this._interfaceTextStyleComponent); + + @override + TextStyle textStyle(Room room) { + return _interfaceTextStyleComponent.textStyle(room); + } + + @override + ChatListTitleTextStyleComponent get interfaceTextStyleComponent => + _interfaceTextStyleComponent; +} + +class ReadChatListTitleTextStyleDecorator + implements ChatListTitleTextStyleComponent { + @override + TextStyle textStyle(Room room) { + return LinagoraTextStyle.material().bodyLarge2.copyWith( + color: LinagoraSysColors.material().onSurface, + ); + } +} + +class UnreadChatListTitleTextStyleDecorator + implements ChatListTitleTextStyleDecorator { + final ChatListTitleTextStyleComponent _interfaceTextStyleComponent; + + UnreadChatListTitleTextStyleDecorator(this._interfaceTextStyleComponent); + + @override + TextStyle textStyle(Room room) { + if (room.isUnreadOrInvited) { + return _interfaceTextStyleComponent.textStyle(room).merge( + LinagoraTextStyle.material().bodyLarge1.copyWith( + color: LinagoraSysColors.material().onSurface, + ), + ); + } else { + return _interfaceTextStyleComponent.textStyle(room); + } + } + + @override + ChatListTitleTextStyleComponent get interfaceTextStyleComponent => + _interfaceTextStyleComponent; +} + +class MuteChatListTitleTextStyleDecorator + implements ChatListTitleTextStyleDecorator { + final ChatListTitleTextStyleComponent _interfaceTextStyleComponent; + + MuteChatListTitleTextStyleDecorator(this._interfaceTextStyleComponent); + + @override + TextStyle textStyle(Room room) { + final isMuted = room.pushRuleState != PushRuleState.notify; + if (isMuted) { + return _interfaceTextStyleComponent.textStyle(room).copyWith( + color: LinagoraRefColors.material().tertiary[20], + ); + } else { + return _interfaceTextStyleComponent.textStyle(room); + } + } + + @override + ChatListTitleTextStyleComponent get interfaceTextStyleComponent => + _interfaceTextStyleComponent; +} diff --git a/lib/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_view.dart b/lib/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_view.dart new file mode 100644 index 0000000000..86922dcff3 --- /dev/null +++ b/lib/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_view.dart @@ -0,0 +1,11 @@ +import 'package:fluffychat/presentation/decorators/chat_list/title_text_style_decorator/title_text_style_decorator.dart'; + +class ChatLitTitleTextStyleView { + static ChatListTitleTextStyle textStyle = ChatListTitleTextStyle( + MuteChatListTitleTextStyleDecorator( + UnreadChatListTitleTextStyleDecorator( + ReadChatListTitleTextStyleDecorator(), + ), + ), + ); +} diff --git a/lib/presentation/enum/chat/popup_menu_item_web_enum.dart b/lib/presentation/enum/chat/popup_menu_item_web_enum.dart new file mode 100644 index 0000000000..f34755c591 --- /dev/null +++ b/lib/presentation/enum/chat/popup_menu_item_web_enum.dart @@ -0,0 +1,5 @@ +enum InputBarContextMenu { + copy, + cut, + paste, +} diff --git a/lib/presentation/enum/chat_list/chat_list_enum.dart b/lib/presentation/enum/chat_list/chat_list_enum.dart index c8ef378a8d..8701c2e77f 100644 --- a/lib/presentation/enum/chat_list/chat_list_enum.dart +++ b/lib/presentation/enum/chat_list/chat_list_enum.dart @@ -101,7 +101,11 @@ enum ChatListSelectionActions { return L10n.of(context)!.unmuteThisMessage; } case ChatListSelectionActions.pin: - return L10n.of(context)!.pinThisMessage; + if (room.isFavourite) { + return L10n.of(context)!.unpinThisMessage; + } else { + return L10n.of(context)!.pinThisMessage; + } case ChatListSelectionActions.more: return L10n.of(context)!.more; } diff --git a/lib/presentation/enum/settings/settings_enum.dart b/lib/presentation/enum/settings/settings_enum.dart index 889993dc4e..59faf9f487 100644 --- a/lib/presentation/enum/settings/settings_enum.dart +++ b/lib/presentation/enum/settings/settings_enum.dart @@ -10,6 +10,7 @@ enum SettingEnum { appLanguage, devices, help, + about, logout; String titleSettings(BuildContext context) { @@ -28,6 +29,8 @@ enum SettingEnum { return L10n.of(context)!.devices; case SettingEnum.help: return L10n.of(context)!.help; + case SettingEnum.about: + return L10n.of(context)!.about; case SettingEnum.logout: return L10n.of(context)!.logout; default: @@ -72,6 +75,8 @@ enum SettingEnum { return Icons.devices; case SettingEnum.help: return Icons.question_mark; + case SettingEnum.about: + return Icons.privacy_tip_outlined; case SettingEnum.logout: return Icons.logout_outlined; default: diff --git a/lib/presentation/extensions/image_extension.dart b/lib/presentation/extensions/image_extension.dart new file mode 100644 index 0000000000..b3f55d992e --- /dev/null +++ b/lib/presentation/extensions/image_extension.dart @@ -0,0 +1,20 @@ +import 'dart:async'; + +import 'package:flutter/widgets.dart'; + +extension ImageExtension on Image { + Future calculateImageDimension() { + final completer = Completer(); + image.resolve(const ImageConfiguration()).addListener( + ImageStreamListener( + (ImageInfo image, bool synchronousCall) { + final myImage = image.image; + final Size size = + Size(myImage.width.toDouble(), myImage.height.toDouble()); + completer.complete(size); + }, + ), + ); + return completer.future; + } +} diff --git a/lib/presentation/extensions/send_file_extension.dart b/lib/presentation/extensions/send_file_extension.dart index 4ea0ecf162..ab76829a21 100644 --- a/lib/presentation/extensions/send_file_extension.dart +++ b/lib/presentation/extensions/send_file_extension.dart @@ -1,18 +1,23 @@ +import 'dart:async'; import 'dart:io'; import 'package:collection/collection.dart'; import 'package:fluffychat/data/network/media/media_api.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/domain/model/room/room_extension.dart'; +import 'package:fluffychat/presentation/extensions/image_extension.dart'; import 'package:fluffychat/presentation/fake_sending_file_info.dart'; import 'package:fluffychat/presentation/model/file/file_asset_entity.dart'; import 'package:fluffychat/utils/date_time_extension.dart'; +import 'package:flutter/widgets.dart'; import 'package:image/image.dart' as img; import 'package:blurhash_dart/blurhash_dart.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; import 'package:path_provider/path_provider.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart'; +import 'package:video_thumbnail/video_thumbnail.dart'; typedef TransactionId = String; @@ -89,8 +94,30 @@ extension SendFileExtension on Room { fileInfo, targetPath: tempThumbnailFile.path, ); + fileInfo = ImageFileInfo( + fileInfo.fileName, + fileInfo.filePath, + fileInfo.fileSize, + width: thumbnail?.width, + height: thumbnail?.height, + ); + storePlaceholderFileInMem( + fileInfo: fileInfo, + txid: txid, + ); + fakeImageEvent = await sendFakeImagePickerFileEvent( + fileInfo, + txid: txid, + messageType: msgType, + inReplyTo: inReplyTo, + editEventId: editEventId, + shrinkImageMaxDimension: shrinkImageMaxDimension, + extraContent: extraContent, + ); - if (thumbnail != null && fileInfo.fileSize < thumbnail.fileSize) { + if (thumbnail != null && + fileInfo.fileSize > 0 && + fileInfo.fileSize < thumbnail.fileSize) { thumbnail = null; // in this case, the thumbnail is not usefull } } else if (fileInfo is VideoFileInfo) { @@ -100,6 +127,29 @@ extension SendFileExtension on Room { FileSendingStatus.generatingThumbnail.name, ); thumbnail ??= await _getThumbnailVideo(tempThumbnailFile, fileInfo); + if (fileInfo.width == null || fileInfo.height == null) { + fileInfo = VideoFileInfo( + fileInfo.fileName, + fileInfo.filePath, + fileInfo.fileSize, + imagePlaceholderBytes: fileInfo.imagePlaceholderBytes, + width: thumbnail.width, + height: thumbnail.height, + ); + storePlaceholderFileInMem( + fileInfo: fileInfo, + txid: txid, + ); + fakeImageEvent = await sendFakeImagePickerFileEvent( + fileInfo, + txid: txid, + messageType: msgType, + inReplyTo: inReplyTo, + editEventId: editEventId, + shrinkImageMaxDimension: shrinkImageMaxDimension, + extraContent: extraContent, + ); + } } EncryptedFileInfo? encryptedFileInfo; @@ -211,6 +261,7 @@ extension SendFileExtension on Room { 'url': uploadResp.toString(), if (encryptedFileInfo != null) 'file': encryptedFileInfo.toJson(), 'info': { + ...thumbnail?.metadata ?? {}, ...fileInfo.metadata, if (thumbnail != null && encryptedThumbnail == null) 'thumbnail_url': thumbnailUploadResp.toString(), @@ -351,15 +402,26 @@ extension SendFileExtension on Room { originalFile.filePath, targetPath, quality: AppConfig.thumbnailQuality, + format: CompressFormat.jpeg, ); if (result == null) return null; final size = await result.length(); + var width = originalFile.width; + var height = originalFile.height; + if (width == null || height == null) { + final imageDimension = await runBenchmarked( + '_calculateImageDimension', + () => _calculateImageDimension(result.path), + ); + width = imageDimension.width.toInt(); + height = imageDimension.height.toInt(); + } return ImageFileInfo( result.name, result.path, size, - width: originalFile.width, - height: originalFile.height, + width: width, + height: height, ); } catch (e) { Logs().e('Error while generating thumbnail', e); @@ -392,12 +454,36 @@ extension SendFileExtension on Room { File tempThumbnailFile, VideoFileInfo fileInfo, ) async { - await tempThumbnailFile.writeAsBytes(fileInfo.imagePlaceholderBytes); + final int fileSize; + if (fileInfo.imagePlaceholderBytes.isNotEmpty) { + await tempThumbnailFile.writeAsBytes(fileInfo.imagePlaceholderBytes); + fileSize = fileInfo.imagePlaceholderBytes.lengthInBytes; + } else { + await VideoThumbnail.thumbnailFile( + video: fileInfo.filePath, + imageFormat: ImageFormat.JPEG, + quality: AppConfig.thumbnailQuality, + thumbnailPath: tempThumbnailFile.path, + ); + fileSize = await tempThumbnailFile.length(); + } + var width = fileInfo.width; + var height = fileInfo.height; + if (width == null || height == null) { + final imageDimension = await runBenchmarked( + '_calculateImageDimension', + () => _calculateImageBytesDimension(fileInfo.imagePlaceholderBytes), + ); + width = imageDimension.width.toInt(); + height = imageDimension.height.toInt(); + } Logs().d('Video thumbnail generated', tempThumbnailFile.path); final newThumbnail = ImageFileInfo( tempThumbnailFile.path.split("/").last, tempThumbnailFile.path, - fileInfo.imagePlaceholderBytes.lengthInBytes, + fileSize, + width: width, + height: height, ); return newThumbnail; } @@ -411,4 +497,12 @@ extension SendFileExtension on Room { .unsigned![key] = value; await handleImageFakeSync(fakeImageEvent); } + + Future _calculateImageDimension(String filePath) { + return Image.file(File(filePath)).calculateImageDimension(); + } + + Future _calculateImageBytesDimension(Uint8List bytes) { + return Image.memory(bytes).calculateImageDimension(); + } } diff --git a/lib/presentation/extensions/shared_media_file_extension.dart b/lib/presentation/extensions/shared_media_file_extension.dart new file mode 100644 index 0000000000..3f064277be --- /dev/null +++ b/lib/presentation/extensions/shared_media_file_extension.dart @@ -0,0 +1,34 @@ +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:matrix/matrix.dart'; +import 'package:receive_sharing_intent/receive_sharing_intent.dart'; + +extension SharedMediaFileExtension on SharedMediaFile { + MatrixFile toMatrixFile() { + if (type == SharedMediaType.IMAGE) { + return MatrixImageFile( + bytes: null, + name: path.split("/").last, + filePath: path, + ); + } + if (type == SharedMediaType.VIDEO) { + Uint8List? thumbnailBytes; + if (thumbnail != null) { + thumbnailBytes = File(thumbnail!).readAsBytesSync(); + } + return MatrixVideoFile( + bytes: thumbnailBytes, + name: path.split("/").last, + filePath: path, + duration: duration, + ); + } + return MatrixFile( + bytes: null, + name: path.split("/").last, + filePath: path, + ); + } +} diff --git a/lib/presentation/extensions/text_editting_controller_extension.dart b/lib/presentation/extensions/text_editting_controller_extension.dart new file mode 100644 index 0000000000..25ed49e0ce --- /dev/null +++ b/lib/presentation/extensions/text_editting_controller_extension.dart @@ -0,0 +1,43 @@ +import 'package:fluffychat/utils/clipboard.dart'; +import 'package:flutter/material.dart'; + +extension TextEdittingControllerExtension on TextEditingController { + Future pasteText() async { + final start = selection.start; + final end = selection.end; + Clipboard.instance.initReader(); + final pastedText = await Clipboard.instance.pasteText(); + if (pastedText != null) { + if (start == -1 || end == -1) { + text = pastedText + text; + selection = TextSelection.collapsed(offset: text.length); + return; + } + if (start == end) { + final startText = text.substring(0, start); + final trailingText = text.substring(end, text.length); + text = startText + pastedText + trailingText; + } else { + text = text.replaceRange(start, end, pastedText); + } + selection = TextSelection.collapsed(offset: end + pastedText.length); + } + } + + Future copyText() async { + final start = selection.start; + final end = selection.end; + if (start < end) { + await Clipboard.instance.copyText(text.substring(start, end)); + } + } + + Future cutText() async { + //TO-DO: + } + + void addNewLine() { + text = '$text\n'; + selection = TextSelection.collapsed(offset: text.length); + } +} diff --git a/lib/pages/chat_list/chat_list_item_mixin.dart b/lib/presentation/mixins/chat_list_item_mixin.dart similarity index 64% rename from lib/pages/chat_list/chat_list_item_mixin.dart rename to lib/presentation/mixins/chat_list_item_mixin.dart index 4034702aec..d25c525a7a 100644 --- a/lib/pages/chat_list/chat_list_item_mixin.dart +++ b/lib/presentation/mixins/chat_list_item_mixin.dart @@ -1,8 +1,7 @@ -import 'package:fluffychat/pages/chat_list/chat_list_item_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/matrix_locals.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:matrix/matrix.dart'; mixin ChatListItemMixin { @@ -37,12 +36,7 @@ mixin ChatListItemMixin { softWrap: false, maxLines: isGroup ? 1 : 2, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - letterSpacing: 0.4, - color: unread - ? Theme.of(context).colorScheme.onSurface - : LinagoraRefColors.material().neutral[50], - ), + style: ChatLitSubSubtitleTextStyleView.textStyle.textStyle(room), ); }, ); @@ -63,7 +57,7 @@ mixin ChatListItemMixin { color: Theme.of(context).colorScheme.primary, ), ), - maxLines: 1, + maxLines: 2, softWrap: true, ), ), @@ -71,7 +65,7 @@ mixin ChatListItemMixin { ); } - RenderObjectWidget lastSenderWidget(Room room, bool isGroup, bool unread) { + RenderObjectWidget lastSenderWidget(Room room, bool isGroup) { return isGroup ? Row( children: [ @@ -85,14 +79,8 @@ mixin ChatListItemMixin { overflow: TextOverflow.ellipsis, maxLines: 1, softWrap: false, - style: Theme.of(context).textTheme.labelLarge?.merge( - TextStyle( - overflow: TextOverflow.ellipsis, - color: unread - ? Theme.of(context).colorScheme.onSurface - : ChatListItemStyle.readMessageColor, - ), - ), + style: ChatLitSubSubtitleTextStyleView.textStyle + .textStyle(room), ); }, ), @@ -102,4 +90,35 @@ mixin ChatListItemMixin { ) : const SizedBox.shrink(); } + + Widget chatListItemSubtitleForGroup({ + required Room room, + }) { + return FutureBuilder( + future: room.lastEvent?.fetchSenderUser(), + builder: (context, snapshot) { + if (snapshot.data == null) return const SizedBox.shrink(); + final youAreInvitedToThisChat = + L10n.of(context)!.youAreInvitedToThisChat; + final subscriptions = room.membership == Membership.invite + ? youAreInvitedToThisChat + : room.lastEvent?.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + ) ?? + L10n.of(context)!.emptyChat; + + return Text( + "${snapshot.data!.calcDisplayname()}: $subscriptions", + softWrap: false, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: ChatLitSubSubtitleTextStyleView.textStyle.textStyle(room), + ); + }, + ); + } } diff --git a/lib/presentation/mixins/paste_image_mixin.dart b/lib/presentation/mixins/paste_image_mixin.dart new file mode 100644 index 0000000000..99ad7f1056 --- /dev/null +++ b/lib/presentation/mixins/paste_image_mixin.dart @@ -0,0 +1,57 @@ +import 'dart:typed_data'; + +import 'package:fluffychat/pages/chat/send_file_dialog.dart'; +import 'package:fluffychat/presentation/model/clipboard/clipboard_image_info.dart'; +import 'package:fluffychat/utils/clipboard.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/utils/twake_snackbar.dart'; +import 'package:flutter/material.dart'; +import 'package:matrix/matrix.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +mixin PasteImageMixin { + Future pasteImage(BuildContext context, Room room) async { + if (!(await Clipboard.instance.isReadableImageFormat())) { + TwakeSnackBar.show(context, L10n.of(context)!.fileFormatNotSupported); + Logs().e('PasteImageMixin::pasteImage(): not readable image format'); + return; + } + Uint8List? imageData; + ClipboardImageInfo? imageClipboard; + if (PlatformInfos.isWeb) { + imageData = await Clipboard.instance.pasteImageUsingBytes(); + } else { + imageClipboard = await Clipboard.instance.pasteImageUsingStream(); + if (imageClipboard == null) { + TwakeSnackBar.show(context, L10n.of(context)!.pasteImageFailed); + return; + } + // FIXME: need to update the SendFileDialog to have FileInfo inside + // after update we can use stream to read files instead of convert into raw image data + final data = await imageClipboard.stream.toList(); + imageData = Uint8List.fromList( + data.expand((Uint8List uint8List) => uint8List).toList(), + ); + } + if (imageData == null || imageData.isEmpty) { + TwakeSnackBar.show(context, L10n.of(context)!.pasteImageFailed); + return; + } + + await showDialog( + context: context, + useRootNavigator: PlatformInfos.isWeb, + builder: (context) { + return SendFileDialog( + room: room, + files: [ + MatrixImageFile( + name: imageClipboard?.fileName ?? 'copied', + bytes: imageData, + ) + ], + ); + }, + ); + } +} diff --git a/lib/presentation/model/clipboard/clipboard_image_info.dart b/lib/presentation/model/clipboard/clipboard_image_info.dart new file mode 100644 index 0000000000..a172e3183f --- /dev/null +++ b/lib/presentation/model/clipboard/clipboard_image_info.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/services.dart'; + +class ClipboardImageInfo with EquatableMixin { + final Stream stream; + + final String? fileName; + + final int? fileSize; + + ClipboardImageInfo({ + required this.stream, + this.fileName, + this.fileSize, + }); + + @override + List get props => [stream, fileName, fileSize]; +} diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 508009fad5..a0f1c7f0c4 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -200,7 +200,10 @@ class BackgroundPush { "content-available": 1, "badge": 1, "sound": "default", - "alert": {"loc-key": "SINGLE_UNREAD", "loc-args": []} + "alert": { + "loc-key": "newMessageInTwake", + "loc-args": [] + } } } } @@ -343,6 +346,7 @@ class BackgroundPush { Future goToRoom(String? roomId) async { try { + _clearAllNavigatorAvailable(); Logs().v('[Push] Attempting to go to room $roomId...'); if (_matrixState == null || roomId == null) { return; @@ -356,6 +360,10 @@ class BackgroundPush { ?.content .tryGet('type') == ClientStoriesExtension.storiesRoomType; + if (client.getRoomById(roomId) == null) { + Logs().v('[Push] Room $roomId not found, syncing...'); + await client.waitForRoomInSync(roomId); + } TwakeApp.router.go('/rooms/$roomId'); } catch (e, s) { Logs().e('[Push] Failed to open room', e, s); @@ -577,4 +585,8 @@ class BackgroundPush { oldTokens: {_pushToken}, ); } + + void _clearAllNavigatorAvailable() { + TwakeApp.router.routerDelegate.pop(); + } } diff --git a/lib/utils/clipboard.dart b/lib/utils/clipboard.dart new file mode 100644 index 0000000000..95f9859aff --- /dev/null +++ b/lib/utils/clipboard.dart @@ -0,0 +1,171 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:fluffychat/presentation/model/clipboard/clipboard_image_info.dart'; +import 'package:flutter/services.dart'; +import 'package:matrix/matrix.dart'; +import 'package:mime/mime.dart'; +import 'package:super_clipboard/super_clipboard.dart'; + +class Clipboard { + static final _clipboard = Clipboard._(); + + Clipboard._(); + + ClipboardReader? _reader; + + static Clipboard get instance => _clipboard; + + static const allImageFormatsSupported = [ + Formats.png, + Formats.jpeg, + Formats.heic, + Formats.heif, + Formats.svg, + ]; + + Future copyText(String text) async { + final item = DataWriterItem(); + item.add(Formats.plainText(text)); + await ClipboardWriter.instance.write([item]); + } + + Future copyImageAsStream(File image, {String? mimeType}) async { + final item = DataWriterItem(suggestedName: image.path); + final imageStream = image.openRead(); + final mime = mimeType ?? lookupMimeType(image.path); + await imageStream.forEach((data) { + item.add(getFormatFrom(mime)(Uint8List.fromList(data))); + }); + await ClipboardWriter.instance.write([item]); + } + + Future copyImageAsBytes(Uint8List data, {String? mimeType}) async { + final item = DataWriterItem(); + item.add(getFormatFrom(mimeType)(data)); + await ClipboardWriter.instance.write([item]); + } + + Future initReader() async { + _reader = await ClipboardReader.readClipboard(); + } + + Future pasteImageUsingStream() async { + _reader = await ClipboardReader.readClipboard(); + ClipboardImageInfo? imageInfo; + + final readableFormats = _reader!.getFormats(allImageFormatsSupported); + if (readableFormats.isEmpty != false && + readableFormats.first is! SimpleFileFormat) { + return imageInfo; + } + + final c = Completer(); + final progress = _reader!.getFile( + readableFormats.first as SimpleFileFormat, + (file) async { + try { + imageInfo = ClipboardImageInfo( + stream: file.getStream(), + fileName: file.fileName, + fileSize: file.fileSize, + ); + c.complete(imageInfo); + } catch (e) { + Logs().e('Clipboard::pasteImageUsingBytes(): $e'); + c.completeError(e); + } + }, + onError: (e) { + Logs().e('Clipboard::pasteImageUsingBytes(): $e'); + c.completeError(e); + }, + ); + if (progress == null) { + c.complete(null); + } + return c.future; + } + + Future? pasteImageUsingBytes() async { + _reader = await ClipboardReader.readClipboard(); + final readableFormats = _reader!.getFormats(allImageFormatsSupported); + if (readableFormats.isEmpty != false && + readableFormats.first is! SimpleFileFormat) { + return null; + } + + final c = Completer(); + final progress = _reader!.getFile( + readableFormats.first as SimpleFileFormat, + (file) async { + try { + final all = await file.readAll(); + c.complete(all); + } catch (e) { + Logs().e('Clipboard::pasteImageUsingBytes(): $e'); + c.completeError(e); + } + }, + onError: (e) { + Logs().e('Clipboard::pasteImageUsingBytes(): $e'); + c.completeError(e); + }, + ); + if (progress == null) { + c.complete(null); + } + return c.future; + } + + Future isReadableImageFormat() async { + _reader = await ClipboardReader.readClipboard(); + return _reader!.canProvide(Formats.png) || + _reader!.canProvide(Formats.jpeg) || + _reader!.canProvide(Formats.heic) || + _reader!.canProvide(Formats.heif) || + _reader!.canProvide(Formats.svg); + } + + Future pasteText() async { + _reader = await ClipboardReader.readClipboard(); + String? copied; + + final readersFormat = _reader!.getFormats(Formats.standardFormats); + if (readersFormat.isEmpty) { + return copied; + } + + final c = Completer(); + final progress = _reader!.getValue( + Formats.plainText, + (value) { + copied = value; + c.complete(copied); + }, + onError: (error) { + Logs().e('Clipboard::readText(): $error'); + c.completeError(error); + }, + ); + if (progress == null) { + c.completeError('Clipboard::readText(): error'); + } + return c.future; + } + + SimpleFileFormat getFormatFrom(String? mimeType) { + switch (mimeType) { + case 'image/png': + return Formats.png; + case 'image/jpeg': + return Formats.jpeg; + case 'image/heic': + return Formats.heic; + case 'image/heif': + return Formats.heif; + default: + return Formats.plainTextFile; + } + } +} diff --git a/lib/utils/common_helper.dart b/lib/utils/common_helper.dart new file mode 100644 index 0000000000..1f9d7d7523 --- /dev/null +++ b/lib/utils/common_helper.dart @@ -0,0 +1,7 @@ +T? tryCast(dynamic value, {T? fallback}) { + if (value != null && value is T) { + return value; + } + + return fallback; +} diff --git a/lib/utils/date_time_extension.dart b/lib/utils/date_time_extension.dart index 44b8b47d0b..61cac8d127 100644 --- a/lib/utils/date_time_extension.dart +++ b/lib/utils/date_time_extension.dart @@ -72,9 +72,9 @@ extension DateTimeExtension on DateTime { return L10n.of(context)!.sunday; } } else if (year == DateTime.now().year) { - return DateFormat("MMMM d").format(this); + return DateFormat("MMM d").format(this); } else { - return DateFormat("MMMM d, y").format(this); + return DateFormat("MM/dd/yyyy").format(this); } return L10n.of(context)!.dateWithYear( year.toString(), @@ -126,4 +126,14 @@ extension DateTimeExtension on DateTime { String getFormattedCurrentDateTime() { return millisecondsSinceEpoch.toString(); } + + bool isLessThanOneHourAgo({DateTime? other}) { + other ??= DateTime.now(); + return other.difference(this) < const Duration(hours: 1); + } + + bool isLessThanTenHoursAgo({DateTime? other}) { + other ??= DateTime.now(); + return other.difference(this) < const Duration(hours: 10); + } } diff --git a/lib/utils/matrix_sdk_extensions/matrix_locals.dart b/lib/utils/matrix_sdk_extensions/matrix_locals.dart index 11f6c46ad8..73e2edb112 100644 --- a/lib/utils/matrix_sdk_extensions/matrix_locals.dart +++ b/lib/utils/matrix_sdk_extensions/matrix_locals.dart @@ -182,8 +182,8 @@ class MatrixLocals extends MatrixLocalizations { String get noPermission => l10n.noKeyForThisMessage; @override - String redactedAnEvent(Event redactedEvent) { - return l10n.redactedAnEvent(redactedEvent); + String redactedAnEvent(String displayName) { + return l10n.redactedAnEvent(displayName); } @override @@ -192,8 +192,8 @@ class MatrixLocals extends MatrixLocalizations { } @override - String removedBy(Event redactedEvent) { - return l10n.removedBy(redactedEvent); + String removedBy(String displayName) { + return l10n.removedBy(displayName); } @override diff --git a/lib/utils/one_time_debouncer.dart b/lib/utils/one_time_debouncer.dart new file mode 100644 index 0000000000..3f8960c7b7 --- /dev/null +++ b/lib/utils/one_time_debouncer.dart @@ -0,0 +1,18 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +class OneTimeDebouncer { + final int milliseconds; + Timer? _timer; + + OneTimeDebouncer({required this.milliseconds}); + + void run(VoidCallback action) { + if (_timer != null) { + _timer!.cancel(); + } + + _timer = Timer(Duration(milliseconds: milliseconds), action); + } +} diff --git a/lib/utils/permission_service.dart b/lib/utils/permission_service.dart index 16043277c0..be9dcd0d65 100644 --- a/lib/utils/permission_service.dart +++ b/lib/utils/permission_service.dart @@ -31,6 +31,10 @@ class PermissionHandlerService { return (await _deviceInfoPlugin.androidInfo).version.sdkInt; } + Future noNeedStoragePermission() async { + return Platform.isAndroid && (await _getCurrentAndroidVersion() >= 33); + } + Future requestPermissionForCameraActions() async { final currentStatus = await Permission.camera.status; if (currentStatus == PermissionStatus.denied || diff --git a/lib/utils/platform_infos.dart b/lib/utils/platform_infos.dart index 493ed4da71..a86bc94aba 100644 --- a/lib/utils/platform_infos.dart +++ b/lib/utils/platform_infos.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:universal_html/html.dart' as html; import '../config/app_config.dart'; abstract class PlatformInfos { @@ -28,6 +29,12 @@ abstract class PlatformInfos { static bool get platformCanRecord => (isMobile || isMacOS); + static bool get isMacKeyboardPlatform => + isMacOS || + (kIsWeb && + html.window.navigator.platform != null && + html.window.navigator.platform!.toLowerCase().contains('mac')); + static String get clientName => '${AppConfig.applicationName} ${isWeb ? 'web' : Platform.operatingSystem}${kReleaseMode ? '' : 'Debug'}'; @@ -46,18 +53,19 @@ abstract class PlatformInfos { useRootNavigator: false, children: [ Text('Version: $version'), - OutlinedButton( - onPressed: () => UrlLauncher(context, AppConfig.sourceCodeUrl) - .openUrlInAppBrowser(), - child: Text(L10n.of(context)!.sourceCode), - ), - OutlinedButton( - onPressed: () => UrlLauncher(context, AppConfig.emojiFontUrl) - .openUrlInAppBrowser(), - child: const Text(AppConfig.emojiFontName), + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: OutlinedButton( + onPressed: () => UrlLauncher(context, AppConfig.sourceCodeUrl) + .openUrlInAppBrowser(), + child: Text(L10n.of(context)!.sourceCode), + ), ), OutlinedButton( - onPressed: () => context.go('logs'), + onPressed: () { + context.go('/logs'); + Navigator.of(context).pop(); + }, child: const Text('Logs'), ), ], diff --git a/lib/utils/push_helper.dart b/lib/utils/push_helper.dart index c8220d12a6..62beeadfb4 100644 --- a/lib/utils/push_helper.dart +++ b/lib/utils/push_helper.dart @@ -48,7 +48,7 @@ Future pushHelper( l10n ??= lookupL10n(const Locale('en')); flutterLocalNotificationsPlugin.show( 0, - l10n.newMessageInFluffyChat, + l10n.newMessageInTwake, l10n.openAppToReadMessages, NotificationDetails( iOS: const DarwinNotificationDetails(), @@ -151,7 +151,7 @@ Future _tryPushHelper( // Calculate the body final body = event.type == EventTypes.Encrypted - ? l10n.newMessageInFluffyChat + ? l10n.newMessageInTwake : await event.calcLocalizedBody( matrixLocals, plaintextBody: true, diff --git a/lib/utils/room_status_extension.dart b/lib/utils/room_status_extension.dart index 433159a399..a76a2caf6f 100644 --- a/lib/utils/room_status_extension.dart +++ b/lib/utils/room_status_extension.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/utils/date_time_extension.dart'; import 'package:fluffychat/utils/string_extension.dart'; import 'package:flutter/widgets.dart'; @@ -10,50 +11,15 @@ extension RoomStatusExtension on Room { CachedPresence? get directChatPresence => client.presences[directChatMatrixID]; - String getLocalizedStatus(BuildContext context) { - if (isDirectChat) { - final directChatPresence = this.directChatPresence; - if (directChatPresence != null) { - if (directChatPresence.currentlyActive == true) { - return L10n.of(context)!.onlineStatus; - } - if (directChatPresence.lastActiveTimestamp == null) { - return L10n.of(context)!.onlineLongTimeAgo; - } - final time = directChatPresence.lastActiveTimestamp!; + Stream get directChatPresenceStream => + client.onPresenceChanged.stream; - if (DateTime.now().isBefore(time.add(const Duration(hours: 1)))) { - return L10n.of(context)! - .onlineMinAgo(DateTime.now().difference(time).inMinutes); - } else if (DateTime.now() - .isBefore(time.add(const Duration(hours: 24)))) { - final timeOffline = DateTime.now().difference(time); - return L10n.of(context)!.onlineHourAgo( - timeOffline.inHours, - timeOffline.inMinutes - (timeOffline.inHours * 60), - ); - } else if (DateTime.now().isBefore(time.add(const Duration(days: 7)))) { - final timeOffline = DateTime.now().difference(time); - return L10n.of(context)!.onlineDayAgo(timeOffline.inDays); - } else if (DateTime.now() - .isBefore(time.add(const Duration(days: 30)))) { - final timeOffline = DateTime.now().difference(time); - return L10n.of(context)! - .onlineWeekAgo((timeOffline.inDays / 7).truncate()); - } else if (DateTime.now() - .isBefore(time.add(const Duration(days: 365)))) { - final timeOffline = DateTime.now().difference(time); - return L10n.of(context)! - .onlineMonthAgo((timeOffline.inDays / 30).truncate()); - } - } - return L10n.of(context)!.onlineLongTimeAgo; + String getLocalizedStatus(BuildContext context, {CachedPresence? presence}) { + if (isDirectChat) { + return _getLocalizedStatusDirectChat(presence, context); } - final totalMembers = - (summary.mInvitedMemberCount ?? 0) + (summary.mJoinedMemberCount ?? 0); - - return L10n.of(context)!.membersCount(totalMembers.toString()); + return _getLocalizedStatusGroupChat(context); } String getLocalizedTypingText(BuildContext context) { @@ -120,4 +86,43 @@ extension RoomStatusExtension on Room { } return lastReceipts.toList(); } + + bool isTypingText(BuildContext context) { + return getLocalizedTypingText(context).isNotEmpty && + lastEvent?.senderId == client.userID && + lastEvent!.status.isSending; + } + + String _getLocalizedStatusGroupChat(BuildContext context) { + final totalMembers = + (summary.mInvitedMemberCount ?? 0) + (summary.mJoinedMemberCount ?? 0); + + return L10n.of(context)!.membersCount(totalMembers.toString()); + } + + String _getLocalizedStatusDirectChat( + CachedPresence? directChatPresence, + BuildContext context, + ) { + if (directChatPresence != null) { + if (directChatPresence.presence == PresenceType.online) { + return L10n.of(context)!.onlineStatus; + } + final lastActiveDateTime = directChatPresence.lastActiveTimestamp; + final currentDateTime = DateTime.now(); + if (lastActiveDateTime != null) { + if (lastActiveDateTime.isLessThanOneHourAgo()) { + return L10n.of(context)!.onlineMinAgo( + currentDateTime.difference(lastActiveDateTime).inMinutes, + ); + } else if (lastActiveDateTime.isLessThanTenHoursAgo()) { + final timeOffline = currentDateTime.difference(lastActiveDateTime); + return L10n.of(context)!.onlineHourAgo( + (timeOffline.inMinutes / 60).round(), + ); + } + } + } + return L10n.of(context)!.offline; + } } diff --git a/lib/widgets/app_bars/searchable_app_bar.dart b/lib/widgets/app_bars/searchable_app_bar.dart index 80f2b89e62..2ee696b852 100644 --- a/lib/widgets/app_bars/searchable_app_bar.dart +++ b/lib/widgets/app_bars/searchable_app_bar.dart @@ -79,7 +79,13 @@ class SearchableAppBar extends StatelessWidget { if (isFullScreen == true) ...[ TwakeIconButton( icon: Icons.arrow_back, - onTap: () => context.pop(), + onTap: () { + if (context.canPop()) { + context.pop(); + } else { + context.go('/rooms'); + } + }, tooltip: L10n.of(context)!.back, paddingAll: 8.0, margin: const EdgeInsets.symmetric(horizontal: 8.0), diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart index f438ee569f..daaadaf719 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold.dart @@ -3,7 +3,6 @@ import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_vie import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; typedef OnOpenSearchPage = Function(); typedef OnCloseSearchPage = Function(); @@ -36,7 +35,6 @@ class AdaptiveScaffoldAppController extends State { ]; void onDestinationSelected(int index) { - _clearSettingsPage(); final destinationType = destinations[index]; activeNavigationBar.value = destinationType; pageController.jumpToPage(index); @@ -82,12 +80,6 @@ class AdaptiveScaffoldAppController extends State { pageController.jumpToPage(activeNavigationBar.value.index); } - void _clearSettingsPage() { - if (activeNavigationBar.value == AdaptiveDestinationEnum.settings) { - context.go('/rooms'); - } - } - MatrixState get matrix => Matrix.of(context); @override diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_route.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_route.dart index 2609fa5a2d..52d3d6ebdd 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_route.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_route.dart @@ -1,6 +1,7 @@ import 'package:fluffychat/utils/extension/build_context_extension.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_appbar.dart'; +import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_route_style.dart'; import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:linagora_design_flutter/linagora_design_flutter.dart'; @@ -39,13 +40,13 @@ class AdaptiveScaffoldRoute extends StatelessWidget { key: breakpointMobileKey, builder: (_) => secondaryBody != null ? _secondaryBodyWidget(context, isWebAndDesktop: false) - : _bodyWidget(isWebAndDesktop: false), + : _bodyWidget(context, isWebAndDesktop: false), ), const WidthPlatformBreakpoint( begin: ResponsiveUtils.minTabletWidth, ): SlotLayout.from( key: breakpointWebAndDesktopKey, - builder: (_) => _bodyWidget(), + builder: (_) => _bodyWidget(context), ) }, ), @@ -76,13 +77,12 @@ class AdaptiveScaffoldRoute extends StatelessWidget { bool isWebAndDesktop = true, }) { return Padding( - padding: isWebAndDesktop - ? const EdgeInsetsDirectional.all(16) - : EdgeInsetsDirectional.zero, + padding: AdaptiveScaffoldRouteStyle.secondaryBodyWidgetPadding( + isWebAndDesktop, + ), child: ClipRRect( - borderRadius: isWebAndDesktop - ? const BorderRadius.all(Radius.circular(16.0)) - : BorderRadius.zero, + borderRadius: + AdaptiveScaffoldRouteStyle.secondaryBodyBorder(isWebAndDesktop), child: Container( decoration: BoxDecoration( color: LinagoraRefColors.material().primary[100], @@ -93,11 +93,15 @@ class AdaptiveScaffoldRoute extends StatelessWidget { ); } - Widget _bodyWidget({bool isWebAndDesktop = true}) { + Widget _bodyWidget( + BuildContext context, { + bool isWebAndDesktop = true, + }) { return Padding( - padding: isWebAndDesktop - ? const EdgeInsetsDirectional.only(bottom: 16, top: 16) - : EdgeInsetsDirectional.zero, + padding: AdaptiveScaffoldRouteStyle.bodyWidgetPadding( + context, + isWebAndDesktop, + ), child: body, ); } diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_route_style.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_route_style.dart new file mode 100644 index 0000000000..a262a87f15 --- /dev/null +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_route_style.dart @@ -0,0 +1,38 @@ +import 'package:fluffychat/di/global/get_it_initializer.dart'; +import 'package:fluffychat/utils/responsive/responsive_utils.dart'; +import 'package:flutter/material.dart'; + +class AdaptiveScaffoldRouteStyle { + static ResponsiveUtils responsiveUtils = getIt.get(); + + static EdgeInsetsDirectional secondaryBodyWidgetPadding( + bool isWebAndDesktop, + ) { + return isWebAndDesktop + ? const EdgeInsetsDirectional.all(16) + : EdgeInsetsDirectional.zero; + } + + static EdgeInsetsDirectional bodyWidgetPadding( + BuildContext context, + bool isWebAndDesktop, + ) { + return isWebAndDesktop + ? EdgeInsetsDirectional.only( + bottom: 16, + top: 16, + start: responsiveUtils.isTablet(context) ? 16 : 0, + ) + : EdgeInsetsDirectional.zero; + } + + static BorderRadiusGeometry secondaryBodyBorder( + bool isWebAndDesktop, + ) { + if (isWebAndDesktop) { + return const BorderRadius.all(Radius.circular(16.0)); + } else { + return BorderRadius.zero; + } + } +} diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart index 235f5347f8..eb692b033d 100644 --- a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/pages/settings_dashboard/settings/settings.dart'; import 'package:fluffychat/utils/responsive/responsive_utils.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_primary_navigation.dart'; import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold.dart'; +import 'package:fluffychat/widgets/layouts/adaptive_layout/adaptive_scaffold_view_style.dart'; import 'package:fluffychat/widgets/layouts/enum/adaptive_destinations_enum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; @@ -77,7 +78,7 @@ class AppScaffoldView extends StatelessWidget { ], Expanded( child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16.0)), + borderRadius: AppScaffoldViewStyle.borderRadiusBody, child: Container( decoration: BoxDecoration( color: LinagoraRefColors.material().primary[100], @@ -95,7 +96,11 @@ class AppScaffoldView extends StatelessWidget { ), ChatList( activeRoomId: activeRoomId, - bottomNavigationBar: _bottomNavigationBarBuilder(context), + bottomNavigationBar: _triggerPageViewBuilder( + navigatorBarType: AdaptiveDestinationEnum.rooms, + navigatorBarWidget: + _bottomNavigationBarBuilder(context), + ), onOpenSearchPage: onOpenSearchPage, ), _triggerPageViewBuilder( @@ -141,7 +146,7 @@ class AppScaffoldView extends StatelessWidget { return SlotLayout( config: { const WidthPlatformBreakpoint( - end: ResponsiveUtils.maxMobileWidth, + end: ResponsiveUtils.minDesktopWidth, ): SlotLayout.from( key: bottomNavigationKey, builder: (_) { diff --git a/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view_style.dart b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view_style.dart new file mode 100644 index 0000000000..7931aaa21a --- /dev/null +++ b/lib/widgets/layouts/adaptive_layout/adaptive_scaffold_view_style.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +class AppScaffoldViewStyle { + static const BorderRadiusGeometry borderRadiusBody = + BorderRadius.all(Radius.circular(16.0)); +} diff --git a/lib/widgets/log_view.dart b/lib/widgets/log_view.dart index 8e62f7b86c..94d9b72d03 100644 --- a/lib/widgets/log_view.dart +++ b/lib/widgets/log_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; @@ -22,7 +23,9 @@ class LogViewerState extends State { backgroundColor: Colors.black, appBar: AppBar( title: Text(logLevel.toString()), - leading: const BackButton(), + leading: BackButton( + onPressed: () => context.go('/'), + ), actions: [ IconButton( icon: const Icon(Icons.zoom_in_outlined), diff --git a/lib/widgets/matrix.dart b/lib/widgets/matrix.dart index b7b5774b4a..875cea54fc 100644 --- a/lib/widgets/matrix.dart +++ b/lib/widgets/matrix.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/domain/model/extensions/homeserver_summary_extensions import 'package:fluffychat/domain/model/tom_configurations.dart'; import 'package:fluffychat/domain/model/tom_server_information.dart'; import 'package:fluffychat/domain/repository/tom_configurations_repository.dart'; +import 'package:fluffychat/pages/chat_list/receive_sharing_intent_mixin.dart'; import 'package:fluffychat/utils/client_manager.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/platform_infos.dart'; @@ -66,7 +67,8 @@ class Matrix extends StatefulWidget { Provider.of(context, listen: false); } -class MatrixState extends State with WidgetsBindingObserver { +class MatrixState extends State + with WidgetsBindingObserver, ReceiveSharingIntentMixin { int _activeClient = -1; String? activeBundle; Store store = Store(); @@ -271,6 +273,7 @@ class MatrixState extends State with WidgetsBindingObserver { super.initState(); WidgetsBinding.instance.addObserver(this); initMatrix(); + initReceiveSharingIntent(); if (PlatformInfos.isWeb) { initConfig().then((_) => initSettings()); } else { @@ -593,6 +596,7 @@ class MatrixState extends State with WidgetsBindingObserver { state != AppLifecycleState.paused; client.backgroundSync = foreground; client.syncPresence = foreground ? null : PresenceType.unavailable; + client.sync(setPresence: client.syncPresence); client.requestHistoryOnLimitedTimeline = !foreground; backgroundPush?.clearAllNotifications(); } @@ -637,9 +641,6 @@ class MatrixState extends State with WidgetsBindingObserver { store .getItemBool(SettingKeys.autoplayImages, AppConfig.autoplayImages) .then((value) => AppConfig.autoplayImages = value); - store - .getItemBool(SettingKeys.sendOnEnter, AppConfig.sendOnEnter) - .then((value) => AppConfig.sendOnEnter = value); store .getItemBool(SettingKeys.experimentalVoip, AppConfig.experimentalVoip) .then((value) => AppConfig.experimentalVoip = value); @@ -648,7 +649,9 @@ class MatrixState extends State with WidgetsBindingObserver { @override void dispose() { WidgetsBinding.instance.removeObserver(this); - + intentDataStreamSubscription?.cancel(); + intentFileStreamSubscription?.cancel(); + intentUriStreamSubscription?.cancel(); onRoomKeyRequestSub.values.map((s) => s.cancel()); onKeyVerificationRequestSub.values.map((s) => s.cancel()); onLoginStateChanged.values.map((s) => s.cancel()); @@ -670,6 +673,9 @@ class MatrixState extends State with WidgetsBindingObserver { child: widget.child, ); } + + @override + MatrixState get matrixState => this; } class FixedThreepidCreds extends ThreepidCreds { diff --git a/lib/widgets/mixins/popup_menu_widget_mixin.dart b/lib/widgets/mixins/popup_menu_widget_mixin.dart index cdb08eaee3..090f7da9db 100644 --- a/lib/widgets/mixins/popup_menu_widget_mixin.dart +++ b/lib/widgets/mixins/popup_menu_widget_mixin.dart @@ -15,13 +15,15 @@ mixin PopupMenuWidgetMixin { TextStyle? styleName, EdgeInsets? padding, OnTapIconButtonCallbackAction? onCallbackAction, + bool isClearCurrentPage = true, }) { return InkWell( onTap: () { /// Pop the current page, snackbar, dialog or bottomsheet in the stack /// will close the currently open snackbar/dialog/bottomsheet AND the current page - TwakeApp.router.routerDelegate.pop(); - + if (isClearCurrentPage) { + TwakeApp.router.routerDelegate.pop(); + } onCallbackAction!.call(); }, child: Padding( diff --git a/lib/widgets/twake_components/twake_header.dart b/lib/widgets/twake_components/twake_header.dart index c8bdaac448..03da9e18a1 100644 --- a/lib/widgets/twake_components/twake_header.dart +++ b/lib/widgets/twake_components/twake_header.dart @@ -35,54 +35,52 @@ class TwakeHeader extends StatelessWidget alignment: TwakeHeaderStyle.alignment(context), child: Row( children: [ - if (!TwakeHeaderStyle.isDesktop(context)) - Expanded( - flex: TwakeHeaderStyle.flexActions, - child: Padding( - padding: TwakeHeaderStyle.leadingPadding, - child: Row( - children: [ - InkWell( - onTap: onClearSelection, - borderRadius: BorderRadius.circular( - TwakeHeaderStyle.closeIconSize, - ), - child: Icon( - Icons.close, - size: TwakeHeaderStyle.closeIconSize, - color: selectMode == SelectMode.select - ? Theme.of(context) - .colorScheme - .onSurfaceVariant - : Colors.transparent, - ), + Expanded( + flex: TwakeHeaderStyle.flexActions, + child: Padding( + padding: TwakeHeaderStyle.leadingPadding, + child: Row( + children: [ + InkWell( + onTap: selectMode == SelectMode.select + ? onClearSelection + : null, + borderRadius: BorderRadius.circular( + TwakeHeaderStyle.closeIconSize, ), - ValueListenableBuilder( - valueListenable: conversationSelectionNotifier, - builder: (context, conversationSelection, _) { - return Padding( - padding: - TwakeHeaderStyle.counterSelectionPadding, - child: Text( - conversationSelection.length.toString(), - style: Theme.of(context) - .textTheme - .bodyLarge - ?.copyWith( - color: selectMode == SelectMode.select - ? Theme.of(context) - .colorScheme - .onSurfaceVariant - : Colors.transparent, - ), - ), - ); - }, + child: Icon( + Icons.close, + size: TwakeHeaderStyle.closeIconSize, + color: selectMode == SelectMode.select + ? Theme.of(context).colorScheme.onSurfaceVariant + : Colors.transparent, ), - ], - ), + ), + ValueListenableBuilder( + valueListenable: conversationSelectionNotifier, + builder: (context, conversationSelection, _) { + return Padding( + padding: TwakeHeaderStyle.counterSelectionPadding, + child: Text( + conversationSelection.length.toString(), + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith( + color: selectMode == SelectMode.select + ? Theme.of(context) + .colorScheme + .onSurfaceVariant + : Colors.transparent, + ), + ), + ); + }, + ), + ], ), ), + ), Expanded( flex: TwakeHeaderStyle.flexTitle, child: Align( @@ -95,37 +93,39 @@ class TwakeHeader extends StatelessWidget ), ), ), - if (!TwakeHeaderStyle.isDesktop(context)) - Expanded( - flex: TwakeHeaderStyle.flexActions, - child: Padding( - padding: TwakeHeaderStyle.actionsPadding, - child: Align( - alignment: Alignment.centerRight, - child: InkWell( - borderRadius: BorderRadius.circular( - TwakeHeaderStyle.textBorderRadius, - ), - onTap: openSelectMode, - child: Padding( - padding: TwakeHeaderStyle.textButtonPadding, - child: Text( - selectMode == SelectMode.normal - ? L10n.of(context)!.edit - : L10n.of(context)!.done, - style: Theme.of(context) - .textTheme - .labelLarge - ?.copyWith( - color: - Theme.of(context).colorScheme.primary, - ), - ), - ), - ), - ), + Expanded( + flex: TwakeHeaderStyle.flexActions, + child: Padding( + padding: TwakeHeaderStyle.actionsPadding, + child: Align( + alignment: Alignment.centerRight, + child: !TwakeHeaderStyle.isDesktop(context) + ? InkWell( + borderRadius: BorderRadius.circular( + TwakeHeaderStyle.textBorderRadius, + ), + onTap: openSelectMode, + child: Padding( + padding: TwakeHeaderStyle.textButtonPadding, + child: Text( + selectMode == SelectMode.normal + ? L10n.of(context)!.edit + : L10n.of(context)!.done, + style: Theme.of(context) + .textTheme + .labelLarge + ?.copyWith( + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + ), + ) + : const SizedBox.shrink(), ), ), + ), ], ), ); diff --git a/lib/widgets/twake_components/twake_smart_refresher.dart b/lib/widgets/twake_components/twake_smart_refresher.dart index 25700d7dc1..89d49bad94 100644 --- a/lib/widgets/twake_components/twake_smart_refresher.dart +++ b/lib/widgets/twake_components/twake_smart_refresher.dart @@ -62,13 +62,13 @@ class _TwakeSmartRefresherController extends State { } Future onRefresh() async { - if (widget.controller.isRefeshing) return; + if (widget.controller.isRefeshing || widget.onRefresh == null) return; widget.controller.onRefresh(); widget.onRefresh?.call(); } Future onLoading() async { - if (widget.controller.isLoading) return; + if (widget.controller.isLoading || widget.onLoading == null) return; widget.controller.onLoading(); await widget.onLoading!(); } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e3fc573553..629505e486 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -15,9 +15,11 @@ #include #include #include +#include #include #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -48,6 +50,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) handy_window_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "HandyWindowPlugin"); handy_window_plugin_register_with_registrar(handy_window_registrar); + g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); + irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); @@ -57,6 +62,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) record_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "RecordLinuxPlugin"); record_linux_plugin_register_with_registrar(record_linux_registrar); + g_autoptr(FlPluginRegistrar) super_native_extensions_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin"); + super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 591a12a865..92d5322adf 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -12,9 +12,11 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_linux flutter_webrtc handy_window + irondash_engine_context media_kit_libs_linux media_kit_video record_linux + super_native_extensions url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index f0c9c8e51f..c2b025e778 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -21,6 +21,7 @@ import flutter_secure_storage_macos import flutter_web_auth import flutter_webrtc import geolocator_apple +import irondash_engine_context import just_audio import macos_ui import macos_window_utils @@ -34,6 +35,7 @@ import screen_brightness_macos import share_plus_macos import shared_preferences_macos import sqflite +import super_native_extensions import url_launcher_macos import video_compress import wakelock_macos @@ -56,6 +58,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FlutterWebAuthPlugin.register(with: registry.registrar(forPlugin: "FlutterWebAuthPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin")) MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) @@ -69,6 +72,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) VideoCompressPlugin.register(with: registry.registrar(forPlugin: "VideoCompressPlugin")) WakelockMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockMacosPlugin")) diff --git a/pubspec.lock b/pubspec.lock index f2b9fb4f9d..9991552ba6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1043,11 +1043,12 @@ packages: flutter_typeahead: dependency: "direct main" description: - name: flutter_typeahead - sha256: f3a5f79d9a056e5108452dbec31d12bbd7f6d25e9097bf0f956e3f8d024e1747 - url: "https://pub.dev" - source: hosted - version: "4.7.0" + path: "." + ref: twake-supported + resolved-ref: "4d79401802bd3528ab1b27ff865916269bb416c8" + url: "https://github.com/linagora/flutter_typeahead.git" + source: git + version: "4.8.0" flutter_web_auth: dependency: "direct main" description: @@ -1367,6 +1368,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + irondash_engine_context: + dependency: transitive + description: + name: irondash_engine_context + sha256: fea21bff36d44a5955beba90619f54b9169884014128ae75f50eba2db03c24a0 + url: "https://pub.dev" + source: hosted + version: "0.3.1" + irondash_message_channel: + dependency: transitive + description: + name: irondash_message_channel + sha256: "500daa1fbe679f7d28a5258df3ff47dab6de352e680dc93c1ca9eae1555d8db5" + url: "https://pub.dev" + source: hosted + version: "0.3.1" isolate: dependency: transitive description: @@ -1534,7 +1551,7 @@ packages: description: path: "." ref: "twake-supported-0.22.4" - resolved-ref: bbc843232affc4b04fb7dbd45a925ddde4a7e878 + resolved-ref: d3fd4dbeb9ecfbee641753cd6267e27de9fe91f2 url: "git@github.com:linagora/matrix-dart-sdk.git" source: git version: "0.22.4" @@ -1558,8 +1575,8 @@ packages: dependency: "direct main" description: path: "." - ref: "feat/add-text-span-builder" - resolved-ref: f159e4de7db4a70fa1830cf7c4a066979aa1464d + ref: twake-supported + resolved-ref: "326f4ef15303bbbd13971912fd84e936b60e521d" url: "https://github.com/linagora/matrix_link_text.git" source: git version: "2.0.0" @@ -1883,6 +1900,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.8.0" + pixel_snap: + dependency: transitive + description: + name: pixel_snap + sha256: "5de3662b926c9bc189578cf90f9d5b350ee61bc8e20e8a91fa1dfdd26c9f5ece" + url: "https://pub.dev" + source: hosted + version: "0.1.2" platform: dependency: transitive description: @@ -2425,6 +2450,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + super_clipboard: + dependency: "direct main" + description: + name: super_clipboard + sha256: "548642d62d691d2ba00850efe0f7a11ce5696e59111658abd4c6cb76b49aa61d" + url: "https://pub.dev" + source: hosted + version: "0.6.4" + super_native_extensions: + dependency: transitive + description: + name: super_native_extensions + sha256: "3bbb95899f848617b819eec1fa00c80a617f1bd469a480b1f0c4dd383da4c5ec" + url: "https://pub.dev" + source: hosted + version: "0.6.4" sync_http: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b471a608a7..a6a8aa30ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fluffychat description: The open digital workplace. publish_to: none -version: 2.3.2+2330 +version: 2.3.4+2330 environment: sdk: '>=3.0.0 <4.0.0' @@ -43,7 +43,11 @@ dependencies: flutter_ringtone_player: ^3.1.1 flutter_secure_storage: ^7.0.1 flutter_svg: ^0.22.0 - flutter_typeahead: ^4.7.0 + # FIXME: change to upstream when https://github.com/AbdulRahmanAlHamali/flutter_typeahead/pull/528 is merge + flutter_typeahead: + git: + url: https://github.com/linagora/flutter_typeahead.git + ref: twake-supported flutter_web_auth: ^0.5.0 # flutter_webrtc: # Until https://github.com/flutter-webrtc/flutter-webrtc/issues/1212 is fixed # git: https://github.com/radzio-it/flutter-webrtc.git @@ -62,7 +66,7 @@ dependencies: keyboard_shortcuts: ^0.1.4 latlong2: ^0.8.1 matrix: - git: + git: url: git@github.com:linagora/matrix-dart-sdk.git ref: twake-supported-0.22.4 matrix_homeserver_recommendations: ^0.3.0 @@ -149,6 +153,7 @@ dependencies: media_kit_libs_video: ^1.0.1 video_player: ^2.7.2 js: ^0.6.7 + super_clipboard: ^0.6.4 dev_dependencies: build_runner: ^2.3.3 dart_code_metrics: ^5.7.3 @@ -200,9 +205,6 @@ flutter: - family: RobotoMono fonts: - asset: fonts/Roboto/RobotoMono-Regular.ttf - - family: NotoEmoji - fonts: - - asset: fonts/NotoEmoji/NotoColorEmoji.ttf - family: Inter fonts: - asset: fonts/Inter/Inter-Thin.ttf @@ -267,12 +269,13 @@ dependency_overrides: url: https://github.com/chandrabezzo/wakelock.git ref: main path: wakelock_windows/ - # TODO: Remove it after this PR merged + # TODO: Remove this part after upstream contribution is merged # https://github.com/Sorunome/matrix_link_text/pull/4 + # https://github.com/Sorunome/matrix_link_text/pull/5 matrix_link_text: - git: + git: url: https://github.com/linagora/matrix_link_text.git - ref: feat/add-text-span-builder + ref: twake-supported cider: link_template: diff --git a/scripts/build-ios.sh b/scripts/build-ios.sh index 7fd791b5e0..0444de1533 100755 --- a/scripts/build-ios.sh +++ b/scripts/build-ios.sh @@ -16,7 +16,7 @@ FLUFFYCHAT_ORIG_TEAM="4NXF6Z997G" ### Rotate IDs ### [ -n "${FLUFFYCHAT_NEW_GROUP}" ] && { # App group IDs - sed -i "" "s/group.${FLUFFYCHAT_ORIG_GROUP}.app/group.${FLUFFYCHAT_NEW_GROUP}.app/g" "ios/FluffyChat Share/FluffyChat Share.entitlements" + sed -i "" "s/group.${FLUFFYCHAT_ORIG_GROUP}.app/group.${FLUFFYCHAT_NEW_GROUP}.app/g" "ios/Twake Share/Twake Share.entitlements" sed -i "" "s/group.${FLUFFYCHAT_ORIG_GROUP}.app/group.${FLUFFYCHAT_NEW_GROUP}.app/g" "ios/Runner/Runner.entitlements" sed -i "" "s/group.${FLUFFYCHAT_ORIG_GROUP}.app/group.${FLUFFYCHAT_NEW_GROUP}.app/g" "ios/Runner.xcodeproj/project.pbxproj" # Bundle identifiers diff --git a/scripts/build-linux-debug.sh b/scripts/build-linux-debug.sh index 49163f22bc..cc99e03a77 100755 --- a/scripts/build-linux-debug.sh +++ b/scripts/build-linux-debug.sh @@ -1,9 +1,9 @@ #!/bin/sh -ve echo "Setup Linux dependencies" -sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libjsoncpp-dev locate libfuse-dev libolm-dev - -# Updating database of locate -sudo updatedb +sudo apt-get install -y clang cmake ninja-build \ + pkg-config libgtk-3-dev liblzma-dev \ + libjsoncpp-dev libfuse-dev \ + libolm-dev libmpv-dev libsecret-1-dev flutter config --enable-linux-desktop flutter clean diff --git a/scripts/build-linux.sh b/scripts/build-linux.sh index 9848b3d9da..c3e3839edb 100755 --- a/scripts/build-linux.sh +++ b/scripts/build-linux.sh @@ -1,9 +1,9 @@ #!/bin/sh -ve echo "Setup Linux dependencies" -sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libjsoncpp-dev locate libfuse-dev libolm-dev - -# Updating database of locate -sudo updatedb +sudo apt-get install -y clang cmake ninja-build \ + pkg-config libgtk-3-dev liblzma-dev \ + libjsoncpp-dev libfuse-dev \ + libolm-dev libmpv-dev libsecret-1-dev flutter config --enable-linux-desktop flutter clean diff --git a/scripts/package-linux-debug.sh b/scripts/package-linux-debug.sh index 45bda4415b..813a8959d8 100755 --- a/scripts/package-linux-debug.sh +++ b/scripts/package-linux-debug.sh @@ -1,10 +1,15 @@ #!/bin/bash -# Setup AppImageTool for packaging -echo "Setting up AppImageTool" -curl -o appimagetool -L "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -chmod +x appimagetool -sudo mv appimagetool /usr/local/bin/ +# Setup appimage-builder for packaging +echo "Setting up appimage-builder" +wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage +chmod +x appimage-builder-x86_64.AppImage +sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder echo "Packaging." -flutter pub global run flutter_distributor:main.dart package --platform linux --targets appimage --skip-clean --flutter-build-args="profile" +export BUILD_TYPE=profile +# This is to set environment variable from a file +# https://stackoverflow.com/a/45971167/8296391 +set -a; . /etc/os-release; set +a +appimage-builder --recipe appimage/AppImageBuilder.yml --skip-tests +mkdir dist && cp ./*.AppImage dist/ diff --git a/scripts/package-linux.sh b/scripts/package-linux.sh index 3f5df909ee..ee8e04f3f4 100755 --- a/scripts/package-linux.sh +++ b/scripts/package-linux.sh @@ -1,10 +1,15 @@ #!/bin/bash -# Setup AppImageTool for packaging -echo "Setting up AppImageTool" -curl -o appimagetool -L "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -chmod +x appimagetool -sudo mv appimagetool /usr/local/bin/ +# Setup appimage-builder for packaging +echo "Setting up appimage-builder" +wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage +chmod +x appimage-builder-x86_64.AppImage +sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder echo "Packaging." -flutter pub global run flutter_distributor:main.dart package --platform linux --targets appimage --skip-clean --flutter-build-args="release" +export BUILD_TYPE=release +# This is to set environment variable from a file +# https://stackoverflow.com/a/45971167/8296391 +set -a; . /etc/os-release; set +a +appimage-builder --recipe appimage/AppImageBuilder.yml --skip-tests +mkdir dist && cp ./*.AppImage dist/ diff --git a/web/index.html b/web/index.html index 5e69da154c..b7127210af 100644 --- a/web/index.html +++ b/web/index.html @@ -37,6 +37,12 @@ name="viewport" /> + + + @@ -68,75 +74,31 @@ alt="" /> - - - + + diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 7c2d17617d..d8a27d3b42 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -14,11 +14,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -38,6 +40,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterWebRTCPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterWebRTCPlugin")); + IrondashEngineContextPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); MediaKitVideoPluginCApiRegisterWithRegistrar( @@ -48,6 +52,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); ScreenBrightnessWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); + SuperNativeExtensionsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 2d0ed4fc1c..30d5e717d8 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -11,11 +11,13 @@ list(APPEND FLUTTER_PLUGIN_LIST file_saver file_selector_windows flutter_webrtc + irondash_engine_context media_kit_libs_windows_video media_kit_video permission_handler_windows record_windows screen_brightness_windows + super_native_extensions url_launcher_windows )